From e3b0918dd9b476b351b5e0b58feac0e5af22047f Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Sat, 17 Jan 2026 17:29:13 +0800 Subject: [PATCH] test(web): add global zustand mock for tests (#31149) --- web/__mocks__/zustand.ts | 56 +++++++++++++++++++ .../access-control.spec.tsx | 21 ------- .../filter-management/index.spec.tsx | 11 ---- .../plugins/readme-panel/index.spec.tsx | 18 ------ web/tsconfig.json | 2 +- web/vitest.setup.ts | 4 ++ 6 files changed, 61 insertions(+), 51 deletions(-) create mode 100644 web/__mocks__/zustand.ts diff --git a/web/__mocks__/zustand.ts b/web/__mocks__/zustand.ts new file mode 100644 index 0000000000..7255e5ef86 --- /dev/null +++ b/web/__mocks__/zustand.ts @@ -0,0 +1,56 @@ +import type * as ZustandExportedTypes from 'zustand' +import { act } from '@testing-library/react' + +export * from 'zustand' + +const { create: actualCreate, createStore: actualCreateStore } + // eslint-disable-next-line antfu/no-top-level-await + = await vi.importActual('zustand') + +export const storeResetFns = new Set<() => void>() + +const createUncurried = ( + stateCreator: ZustandExportedTypes.StateCreator, +) => { + const store = actualCreate(stateCreator) + const initialState = store.getInitialState() + storeResetFns.add(() => { + store.setState(initialState, true) + }) + return store +} + +export const create = (( + stateCreator: ZustandExportedTypes.StateCreator, +) => { + return typeof stateCreator === 'function' + ? createUncurried(stateCreator) + : createUncurried +}) as typeof ZustandExportedTypes.create + +const createStoreUncurried = ( + stateCreator: ZustandExportedTypes.StateCreator, +) => { + const store = actualCreateStore(stateCreator) + const initialState = store.getInitialState() + storeResetFns.add(() => { + store.setState(initialState, true) + }) + return store +} + +export const createStore = (( + stateCreator: ZustandExportedTypes.StateCreator, +) => { + return typeof stateCreator === 'function' + ? createStoreUncurried(stateCreator) + : createStoreUncurried +}) as typeof ZustandExportedTypes.createStore + +afterEach(() => { + act(() => { + storeResetFns.forEach((resetFn) => { + resetFn() + }) + }) +}) diff --git a/web/app/components/app/app-access-control/access-control.spec.tsx b/web/app/components/app/app-access-control/access-control.spec.tsx index b73ed5c266..3950bdf7ee 100644 --- a/web/app/components/app/app-access-control/access-control.spec.tsx +++ b/web/app/components/app/app-access-control/access-control.spec.tsx @@ -3,9 +3,7 @@ import type { App } from '@/types/app' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import useAccessControlStore from '@/context/access-control-store' -import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode, SubjectType } from '@/models/access-control' -import { defaultSystemFeatures } from '@/types/feature' import Toast from '../../base/toast' import AccessControlDialog from './access-control-dialog' import AccessControlItem from './access-control-item' @@ -105,22 +103,6 @@ const memberSubject: Subject = { accountData: baseMember, } as Subject -const resetAccessControlStore = () => { - useAccessControlStore.setState({ - appId: '', - specificGroups: [], - specificMembers: [], - currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS, - selectedGroupsForBreadcrumb: [], - }) -} - -const resetGlobalStore = () => { - useGlobalPublicStore.setState({ - systemFeatures: defaultSystemFeatures, - }) -} - beforeAll(() => { class MockIntersectionObserver { observe = vi.fn(() => undefined) @@ -132,9 +114,6 @@ beforeAll(() => { }) beforeEach(() => { - vi.clearAllMocks() - resetAccessControlStore() - resetGlobalStore() mockMutateAsync.mockResolvedValue(undefined) mockUseUpdateAccessMode.mockReturnValue({ isPending: false, diff --git a/web/app/components/plugins/plugin-page/filter-management/index.spec.tsx b/web/app/components/plugins/plugin-page/filter-management/index.spec.tsx index 58474b4723..b942a360b0 100644 --- a/web/app/components/plugins/plugin-page/filter-management/index.spec.tsx +++ b/web/app/components/plugins/plugin-page/filter-management/index.spec.tsx @@ -144,17 +144,6 @@ describe('constant.ts - Type Definitions', () => { // ==================== store.ts Tests ==================== describe('store.ts - Zustand Store', () => { - beforeEach(() => { - // Reset store to initial state - const { setState } = useStore - setState({ - tagList: [], - categoryList: [], - showTagManagementModal: false, - showCategoryManagementModal: false, - }) - }) - describe('Initial State', () => { it('should have empty tagList initially', () => { const { result } = renderHook(() => useStore(state => state.tagList)) diff --git a/web/app/components/plugins/readme-panel/index.spec.tsx b/web/app/components/plugins/readme-panel/index.spec.tsx index 8d795eac10..5de18c2ed9 100644 --- a/web/app/components/plugins/readme-panel/index.spec.tsx +++ b/web/app/components/plugins/readme-panel/index.spec.tsx @@ -134,13 +134,6 @@ describe('BUILTIN_TOOLS_ARRAY', () => { // Store Tests // ================================ describe('useReadmePanelStore', () => { - beforeEach(() => { - vi.clearAllMocks() - // Reset store state before each test - const { setCurrentPluginDetail } = useReadmePanelStore.getState() - setCurrentPluginDetail() - }) - describe('Initial State', () => { it('should have undefined currentPluginDetail initially', () => { const { currentPluginDetail } = useReadmePanelStore.getState() @@ -228,12 +221,6 @@ describe('useReadmePanelStore', () => { // ReadmeEntrance Component Tests // ================================ describe('ReadmeEntrance', () => { - beforeEach(() => { - vi.clearAllMocks() - // Reset store state - const { setCurrentPluginDetail } = useReadmePanelStore.getState() - setCurrentPluginDetail() - }) // ================================ // Rendering Tests @@ -417,11 +404,6 @@ describe('ReadmeEntrance', () => { // ================================ describe('ReadmePanel', () => { beforeEach(() => { - vi.clearAllMocks() - // Reset store state - const { setCurrentPluginDetail } = useReadmePanelStore.getState() - setCurrentPluginDetail() - // Reset mock mockUsePluginReadme.mockReturnValue({ data: null, isLoading: false, diff --git a/web/tsconfig.json b/web/tsconfig.json index 78c5930aa2..c7aa998644 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "incremental": true, - "target": "es2015", + "target": "es2022", "jsx": "preserve", "lib": [ "dom", diff --git a/web/vitest.setup.ts b/web/vitest.setup.ts index 26dc25bbcf..597ded9559 100644 --- a/web/vitest.setup.ts +++ b/web/vitest.setup.ts @@ -85,6 +85,10 @@ afterEach(() => { // mock next/image to avoid width/height requirements for data URLs vi.mock('next/image') +// mock zustand - auto-resets all stores after each test +// Based on official Zustand testing guide: https://zustand.docs.pmnd.rs/guides/testing +vi.mock('zustand') + // mock react-i18next vi.mock('react-i18next', async () => { const actual = await vi.importActual('react-i18next')