test(web): add global zustand mock for tests (#31149)

This commit is contained in:
yyh
2026-01-17 17:29:13 +08:00
committed by GitHub
parent fad6fa141d
commit e3b0918dd9
6 changed files with 61 additions and 51 deletions

56
web/__mocks__/zustand.ts Normal file
View File

@@ -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<typeof ZustandExportedTypes>('zustand')
export const storeResetFns = new Set<() => void>()
const createUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreate(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}
export const create = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
return typeof stateCreator === 'function'
? createUncurried(stateCreator)
: createUncurried
}) as typeof ZustandExportedTypes.create
const createStoreUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreateStore(stateCreator)
const initialState = store.getInitialState()
storeResetFns.add(() => {
store.setState(initialState, true)
})
return store
}
export const createStore = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
return typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried
}) as typeof ZustandExportedTypes.createStore
afterEach(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn()
})
})
})

View File

@@ -3,9 +3,7 @@ import type { App } from '@/types/app'
import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
import useAccessControlStore from '@/context/access-control-store' import useAccessControlStore from '@/context/access-control-store'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { AccessMode, SubjectType } from '@/models/access-control' import { AccessMode, SubjectType } from '@/models/access-control'
import { defaultSystemFeatures } from '@/types/feature'
import Toast from '../../base/toast' import Toast from '../../base/toast'
import AccessControlDialog from './access-control-dialog' import AccessControlDialog from './access-control-dialog'
import AccessControlItem from './access-control-item' import AccessControlItem from './access-control-item'
@@ -105,22 +103,6 @@ const memberSubject: Subject = {
accountData: baseMember, accountData: baseMember,
} as Subject } as Subject
const resetAccessControlStore = () => {
useAccessControlStore.setState({
appId: '',
specificGroups: [],
specificMembers: [],
currentMenu: AccessMode.SPECIFIC_GROUPS_MEMBERS,
selectedGroupsForBreadcrumb: [],
})
}
const resetGlobalStore = () => {
useGlobalPublicStore.setState({
systemFeatures: defaultSystemFeatures,
})
}
beforeAll(() => { beforeAll(() => {
class MockIntersectionObserver { class MockIntersectionObserver {
observe = vi.fn(() => undefined) observe = vi.fn(() => undefined)
@@ -132,9 +114,6 @@ beforeAll(() => {
}) })
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks()
resetAccessControlStore()
resetGlobalStore()
mockMutateAsync.mockResolvedValue(undefined) mockMutateAsync.mockResolvedValue(undefined)
mockUseUpdateAccessMode.mockReturnValue({ mockUseUpdateAccessMode.mockReturnValue({
isPending: false, isPending: false,

View File

@@ -144,17 +144,6 @@ describe('constant.ts - Type Definitions', () => {
// ==================== store.ts Tests ==================== // ==================== store.ts Tests ====================
describe('store.ts - Zustand Store', () => { describe('store.ts - Zustand Store', () => {
beforeEach(() => {
// Reset store to initial state
const { setState } = useStore
setState({
tagList: [],
categoryList: [],
showTagManagementModal: false,
showCategoryManagementModal: false,
})
})
describe('Initial State', () => { describe('Initial State', () => {
it('should have empty tagList initially', () => { it('should have empty tagList initially', () => {
const { result } = renderHook(() => useStore(state => state.tagList)) const { result } = renderHook(() => useStore(state => state.tagList))

View File

@@ -134,13 +134,6 @@ describe('BUILTIN_TOOLS_ARRAY', () => {
// Store Tests // Store Tests
// ================================ // ================================
describe('useReadmePanelStore', () => { describe('useReadmePanelStore', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset store state before each test
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
setCurrentPluginDetail()
})
describe('Initial State', () => { describe('Initial State', () => {
it('should have undefined currentPluginDetail initially', () => { it('should have undefined currentPluginDetail initially', () => {
const { currentPluginDetail } = useReadmePanelStore.getState() const { currentPluginDetail } = useReadmePanelStore.getState()
@@ -228,12 +221,6 @@ describe('useReadmePanelStore', () => {
// ReadmeEntrance Component Tests // ReadmeEntrance Component Tests
// ================================ // ================================
describe('ReadmeEntrance', () => { describe('ReadmeEntrance', () => {
beforeEach(() => {
vi.clearAllMocks()
// Reset store state
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
setCurrentPluginDetail()
})
// ================================ // ================================
// Rendering Tests // Rendering Tests
@@ -417,11 +404,6 @@ describe('ReadmeEntrance', () => {
// ================================ // ================================
describe('ReadmePanel', () => { describe('ReadmePanel', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks()
// Reset store state
const { setCurrentPluginDetail } = useReadmePanelStore.getState()
setCurrentPluginDetail()
// Reset mock
mockUsePluginReadme.mockReturnValue({ mockUsePluginReadme.mockReturnValue({
data: null, data: null,
isLoading: false, isLoading: false,

View File

@@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"incremental": true, "incremental": true,
"target": "es2015", "target": "es2022",
"jsx": "preserve", "jsx": "preserve",
"lib": [ "lib": [
"dom", "dom",

View File

@@ -85,6 +85,10 @@ afterEach(() => {
// mock next/image to avoid width/height requirements for data URLs // mock next/image to avoid width/height requirements for data URLs
vi.mock('next/image') 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 // mock react-i18next
vi.mock('react-i18next', async () => { vi.mock('react-i18next', async () => {
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next') const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next')