test(client): reset stores from a registry that a test holds complete

resetAllStores read a hand list of seven stores, and a new store simply never
joined it. tests/helpers/storeRegistry.ts now names every Zustand store in
src/ as either reset or left alone with the reason, and a test fails on a
store that is in neither list or on an entry whose store is gone. The set of
stores that are reset is unchanged.
This commit is contained in:
Maurice
2026-10-10 12:17:59 +02:00
parent 2b86d71d51
commit da280f0d2f
3 changed files with 112 additions and 27 deletions
+8 -27
View File
@@ -1,29 +1,13 @@
import type { StoreApi } from 'zustand';
import { useAuthStore } from '../../src/store/authStore';
import { useTripStore } from '../../src/store/tripStore';
import { useSettingsStore } from '../../src/store/settingsStore';
import { useVacayStore } from '../../src/store/vacayStore';
import { useAddonStore } from '../../src/store/addonStore';
import { useInAppNotificationStore } from '../../src/store/inAppNotificationStore';
import { usePermissionsStore } from '../../src/store/permissionsStore';
// Journey store is reset individually in journey tests to avoid circular import issues
import { RESET_STORES } from './storeRegistry';
// Capture initial states at import time (before any test modifies them)
const initialAuthState = useAuthStore.getState();
const initialTripState = useTripStore.getState();
const initialSettingsState = useSettingsStore.getState();
const initialVacayState = useVacayStore.getState();
const initialAddonState = useAddonStore.getState();
const initialNotifState = useInAppNotificationStore.getState();
const initialPermsState = usePermissionsStore.getState();
// The state of each store in the registry when this module was first imported,
// before any test modified it.
const initialStates = Object.entries(RESET_STORES).map(([name, store]) => ({ name, store, state: store.getState() }));
/** Puts every store in RESET_STORES (tests/helpers/storeRegistry.ts) back to its initial state. */
export function resetAllStores(): void {
useAuthStore.setState(initialAuthState, true);
useTripStore.setState(initialTripState, true);
useSettingsStore.setState(initialSettingsState, true);
useVacayStore.setState(initialVacayState, true);
useAddonStore.setState(initialAddonState, true);
useInAppNotificationStore.setState(initialNotifState, true);
usePermissionsStore.setState(initialPermsState, true);
for (const { store, state } of initialStates) store.setState(state, true);
}
/**
@@ -39,9 +23,6 @@ export function resetAllStores(): void {
*/
type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
export function seedStore<T extends object>(
store: Pick<StoreApi<T>, 'setState'>,
state: DeepPartial<T>,
): void {
export function seedStore<T extends object>(store: Pick<StoreApi<T>, 'setState'>, state: DeepPartial<T>): void {
store.setState(state as Partial<T>);
}
+54
View File
@@ -0,0 +1,54 @@
import type { StoreApi } from 'zustand';
import { useAddonStore } from '../../src/store/addonStore';
import { useAuthStore } from '../../src/store/authStore';
import { useInAppNotificationStore } from '../../src/store/inAppNotificationStore';
import { usePermissionsStore } from '../../src/store/permissionsStore';
import { useSettingsStore } from '../../src/store/settingsStore';
import { useTripStore } from '../../src/store/tripStore';
import { useVacayStore } from '../../src/store/vacayStore';
/**
* Every Zustand store in src/, and what resetAllStores does with it.
*
* RESET_STORES are put back to the state they had when this module was first
* imported, before any test touched them. UNRESET_STORES are left alone, each
* with the reason; their tests reset them on their own. The registry test
* (tests/unit/helpers/storeRegistry.test.ts) finds every `export const
* use…Store = create…` under src/ and fails on one that is in neither list,
* so a new store has to be placed here instead of being forgotten by the
* reset and leaking state from one test into the next.
*
* The stores are imported by name rather than through a glob: an eager glob
* would load every store module into each of the hundreds of test files that
* reset, including the ones that mock a store module or import it in a
* particular order (journeyStore), and a store moved from UNRESET to RESET
* changes what those files start from. Each such move is its own change.
*/
type ResettableStore = Pick<StoreApi<object>, 'getState' | 'setState'>;
export const RESET_STORES: Record<string, ResettableStore> = {
useAuthStore,
useTripStore,
useSettingsStore,
useVacayStore,
useAddonStore,
useInAppNotificationStore,
usePermissionsStore,
};
/** Not reset so far; moving one into RESET_STORES changes what every file that resets starts from. */
const NOT_YET = 'never reset by resetAllStores; its own tests set the state they need';
export const UNRESET_STORES: Record<string, string> = {
useJourneyStore: 'reset in the journey tests themselves; importing it here pulls in a circular import',
useBackgroundTasksStore: NOT_YET,
useCollectionStore: NOT_YET,
useDocSyncOfferStore: NOT_YET,
useHelpStore: NOT_YET,
usePluginStore: NOT_YET,
useRoadtripPreferencesStore: NOT_YET,
useSaveToCollectionStore: NOT_YET,
useServerVersionStore: NOT_YET,
useStudioStore: NOT_YET,
useSystemNoticeStore: NOT_YET,
};
@@ -0,0 +1,50 @@
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join, relative } from 'node:path';
import { describe, expect, it } from 'vitest';
import { useAuthStore } from '../../../src/store/authStore';
import { resetAllStores } from '../../helpers/store';
import { RESET_STORES, UNRESET_STORES } from '../../helpers/storeRegistry';
const SRC = join(process.cwd(), 'src');
const STORE = /export const (use[A-Z]\w*Store)\s*=\s*create\b/g;
function sourceFiles(dir: string, out: string[] = []): string[] {
for (const name of readdirSync(dir)) {
const path = join(dir, name);
if (statSync(path).isDirectory()) sourceFiles(path, out);
else if (/\.tsx?$/.test(name) && !/\.(test|spec)\.tsx?$/.test(name)) out.push(path);
}
return out;
}
/** Every Zustand store src/ exports, as store name → file. */
function storesInSrc(): Map<string, string> {
const found = new Map<string, string>();
for (const path of sourceFiles(SRC)) {
for (const match of readFileSync(path, 'utf8').matchAll(STORE)) found.set(match[1], relative(SRC, path));
}
return found;
}
describe('store registry', () => {
it('FE-STORE-REGISTRY-001: places every store src/ exports, so a new one cannot slip past resetAllStores', () => {
const inSrc = storesInSrc();
expect(inSrc.size).toBeGreaterThan(0);
const listed = new Set([...Object.keys(RESET_STORES), ...Object.keys(UNRESET_STORES)]);
const missing = [...inSrc].filter(([name]) => !listed.has(name)).map(([name, file]) => `${name} (${file})`);
expect(missing, 'add each to RESET_STORES or UNRESET_STORES in tests/helpers/storeRegistry.ts').toEqual([]);
const gone = [...listed].filter((name) => !inSrc.has(name));
expect(gone, 'remove each from tests/helpers/storeRegistry.ts').toEqual([]);
});
it('FE-STORE-REGISTRY-002: lists no store as both reset and left alone', () => {
expect(Object.keys(RESET_STORES).filter((name) => name in UNRESET_STORES)).toEqual([]);
});
it('FE-STORE-REGISTRY-003: resetAllStores puts a registered store back to its first state', () => {
const before = useAuthStore.getState();
useAuthStore.setState({ isAuthenticated: !before.isAuthenticated });
resetAllStores();
expect(useAuthStore.getState()).toBe(before);
});
});