From da280f0d2fb06139b144613f74234a273d3c4bbf Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 10 Oct 2026 12:17:59 +0200 Subject: [PATCH] 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. --- client/tests/helpers/store.ts | 35 +++--------- client/tests/helpers/storeRegistry.ts | 54 +++++++++++++++++++ .../tests/unit/helpers/storeRegistry.test.ts | 50 +++++++++++++++++ 3 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 client/tests/helpers/storeRegistry.ts create mode 100644 client/tests/unit/helpers/storeRegistry.test.ts diff --git a/client/tests/helpers/store.ts b/client/tests/helpers/store.ts index e7c8030d2..9a043255f 100644 --- a/client/tests/helpers/store.ts +++ b/client/tests/helpers/store.ts @@ -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 extends object ? { [P in keyof T]?: DeepPartial } : T; -export function seedStore( - store: Pick, 'setState'>, - state: DeepPartial, -): void { +export function seedStore(store: Pick, 'setState'>, state: DeepPartial): void { store.setState(state as Partial); } diff --git a/client/tests/helpers/storeRegistry.ts b/client/tests/helpers/storeRegistry.ts new file mode 100644 index 000000000..9173b7bef --- /dev/null +++ b/client/tests/helpers/storeRegistry.ts @@ -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, 'getState' | 'setState'>; + +export const RESET_STORES: Record = { + 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 = { + 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, +}; diff --git a/client/tests/unit/helpers/storeRegistry.test.ts b/client/tests/unit/helpers/storeRegistry.test.ts new file mode 100644 index 000000000..24618a87c --- /dev/null +++ b/client/tests/unit/helpers/storeRegistry.test.ts @@ -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 { + const found = new Map(); + 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); + }); +});