test(web): refactor tests to use real Zustand stores with global mock

Migrate test files from manual store mocks to real stores with global
zustand mock auto-reset between tests. Use store.setState() to set
test state and check store state directly or use spies for assertions.
This commit is contained in:
yyh
2026-01-17 21:00:35 +08:00
parent b7de61383c
commit ec114b15dc
11 changed files with 67 additions and 82 deletions

View File

@ -7,6 +7,7 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { Inputs, ModelConfig } from '@/models/debug'
import type { PromptVariable } from '@/types/app'
import { fireEvent, render, screen } from '@testing-library/react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
import { AppModeEnum, ModelModeType, Resolution, TransferMethod } from '@/types/app'
import { APP_CHAT_WITH_MULTIPLE_MODEL } from '../types'
@ -21,9 +22,7 @@ type PromptVariableWithMeta = Omit<PromptVariable, 'type' | 'required'> & {
const mockUseDebugConfigurationContext = vi.fn()
const mockUseFeaturesSelector = vi.fn()
const mockUseEventEmitterContext = vi.fn()
const mockUseAppStoreSelector = vi.fn()
const mockEventEmitter = { emit: vi.fn() }
const mockSetShowAppConfigureFeaturesModal = vi.fn()
let capturedChatInputProps: MockChatInputAreaProps | null = null
let modelIdCounter = 0
let featureState: FeatureStoreState
@ -63,10 +62,6 @@ vi.mock('@/context/event-emitter', () => ({
useEventEmitterContextContext: () => mockUseEventEmitterContext(),
}))
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: { setShowAppConfigureFeaturesModal: typeof mockSetShowAppConfigureFeaturesModal }) => unknown) => mockUseAppStoreSelector(selector),
}))
vi.mock('./debug-item', () => ({
default: ({
modelAndParameter,
@ -191,7 +186,6 @@ describe('DebugWithMultipleModel', () => {
featureState = createFeatureState()
mockUseFeaturesSelector.mockImplementation(selector => selector(featureState))
mockUseEventEmitterContext.mockReturnValue({ eventEmitter: mockEventEmitter })
mockUseAppStoreSelector.mockImplementation(selector => selector({ setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal }))
mockUseDebugConfigurationContext.mockReturnValue(createDebugConfiguration())
})
@ -438,7 +432,7 @@ describe('DebugWithMultipleModel', () => {
expect(capturedChatInputProps?.showFileUpload).toBe(false)
expect(capturedChatInputProps?.speechToTextConfig).toEqual(featureState.features.speech2text)
expect(capturedChatInputProps?.visionConfig).toEqual(featureState.features.file)
expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalledWith(true)
expect(useAppStore.getState().showAppConfigureFeaturesModal).toBe(true)
})
it('should render chat input in agent chat mode', () => {

View File

@ -7,6 +7,7 @@ import type { ProviderContextState } from '@/context/provider-context'
import type { DatasetConfigs, ModelConfig } from '@/models/debug'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { createRef } from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ConfigurationMethodEnum, ModelFeatureEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { CollectionType } from '@/app/components/tools/types'
import { PromptMode } from '@/models/debug'
@ -376,15 +377,7 @@ vi.mock('../hooks', () => ({
useFormattingChangedSubscription: mockUseFormattingChangedSubscription,
}))
const mockSetShowAppConfigureFeaturesModal = vi.fn()
vi.mock('@/app/components/app/store', () => ({
useStore: vi.fn((selector?: (state: { setShowAppConfigureFeaturesModal: typeof mockSetShowAppConfigureFeaturesModal }) => unknown) => {
if (typeof selector === 'function')
return selector({ setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal })
return mockSetShowAppConfigureFeaturesModal
}),
}))
// Use real store - global zustand mock will auto-reset between tests
// Mock event emitter context
vi.mock('@/context/event-emitter', () => ({
@ -659,7 +652,7 @@ describe('DebugWithSingleModel', () => {
fireEvent.click(screen.getByTestId('feature-bar-button'))
expect(mockSetShowAppConfigureFeaturesModal).toHaveBeenCalledWith(true)
expect(useAppStore.getState().showAppConfigureFeaturesModal).toBe(true)
})
})

View File

@ -2,14 +2,11 @@ import type { IPromptValuePanelProps } from './index'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useStore } from '@/app/components/app/store'
import ConfigContext from '@/context/debug-configuration'
import { AppModeEnum, ModelModeType, Resolution } from '@/types/app'
import PromptValuePanel from './index'
vi.mock('@/app/components/app/store', () => ({
useStore: vi.fn(),
}))
// Use real store - global zustand mock will auto-reset between tests
vi.mock('@/app/components/base/features/new-feature-panel/feature-bar', () => ({
default: ({ onFeatureBarClick }: { onFeatureBarClick: () => void }) => (
<button type="button" onClick={onFeatureBarClick}>
@ -18,8 +15,6 @@ vi.mock('@/app/components/base/features/new-feature-panel/feature-bar', () => ({
),
}))
const mockSetShowAppConfigureFeaturesModal = vi.fn()
const mockUseStore = vi.mocked(useStore)
const mockSetInputs = vi.fn()
const mockOnSend = vi.fn()
@ -69,20 +64,9 @@ const renderPanel = (options: {
describe('PromptValuePanel', () => {
beforeEach(() => {
mockUseStore.mockImplementation(selector => selector({
setShowAppConfigureFeaturesModal: mockSetShowAppConfigureFeaturesModal,
appSidebarExpand: '',
currentLogModalActiveTab: 'prompt',
showPromptLogModal: false,
showAgentLogModal: false,
setShowPromptLogModal: vi.fn(),
setShowAgentLogModal: vi.fn(),
showMessageLogModal: false,
showAppConfigureFeaturesModal: false,
} as any))
vi.clearAllMocks()
mockSetInputs.mockClear()
mockOnSend.mockClear()
mockSetShowAppConfigureFeaturesModal.mockClear()
})
it('updates inputs, clears values, and triggers run when ready', async () => {

View File

@ -2,6 +2,7 @@ import type { App } from '@/types/app'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import { Plan } from '@/app/components/billing/type'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
@ -17,10 +18,7 @@ vi.mock('next/navigation', () => ({
}),
}))
const mockSetAppDetail = vi.fn()
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: any) => unknown) => selector({ setAppDetail: mockSetAppDetail }),
}))
// Use real store - global zustand mock will auto-reset between tests
const mockSwitchApp = vi.fn()
const mockDeleteApp = vi.fn()
@ -137,9 +135,17 @@ const renderComponent = (overrides: Partial<React.ComponentProps<typeof SwitchAp
}
}
const setAppDetailSpy = vi.fn()
describe('SwitchAppModal', () => {
beforeEach(() => {
vi.clearAllMocks()
// Spy on setAppDetail
const originalSetAppDetail = useAppStore.getState().setAppDetail
setAppDetailSpy.mockImplementation((...args: Parameters<typeof originalSetAppDetail>) => {
originalSetAppDetail(...args)
})
useAppStore.setState({ setAppDetail: setAppDetailSpy as typeof originalSetAppDetail })
mockIsEditor = true
mockEnableBilling = false
mockPlan = {
@ -275,7 +281,7 @@ describe('SwitchAppModal', () => {
})
expect(mockReplace).toHaveBeenCalledWith('/app/new-app-002/workflow')
expect(mockPush).not.toHaveBeenCalled()
expect(mockSetAppDetail).toHaveBeenCalledTimes(1)
expect(setAppDetailSpy).toHaveBeenCalledTimes(1)
})
it('should notify error when switch app fails', async () => {

View File

@ -1,5 +1,6 @@
import { act, fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { AppModeEnum } from '@/types/app'
// Import after mocks
@ -123,18 +124,7 @@ vi.mock('@/service/use-apps', () => ({
}),
}))
// Mock tag store
vi.mock('@/app/components/base/tag-management/store', () => ({
useStore: (selector: (state: { tagList: any[], setTagList: any, showTagManagementModal: boolean, setShowTagManagementModal: any }) => any) => {
const state = {
tagList: [{ id: 'tag-1', name: 'Test Tag', type: 'app' }],
setTagList: vi.fn(),
showTagManagementModal: false,
setShowTagManagementModal: vi.fn(),
}
return selector(state)
},
}))
// Use real tag store - global zustand mock will auto-reset between tests
// Mock tag service to avoid API calls in TagFilter
vi.mock('@/service/tag', () => ({
@ -247,6 +237,11 @@ beforeAll(() => {
describe('List', () => {
beforeEach(() => {
vi.clearAllMocks()
// Set up tag store state
useTagStore.setState({
tagList: [{ id: 'tag-1', name: 'Test Tag', type: 'app', binding_count: 0 }],
showTagManagementModal: false,
})
mockIsCurrentWorkspaceEditor.mockReturnValue(true)
mockIsCurrentWorkspaceDatasetOperator.mockReturnValue(false)
mockDragging = false

View File

@ -92,6 +92,9 @@ vi.mock('@/app/components/workflow/store', () => {
useWorkflowStore: () => ({
getState: () => ({
pipelineId: 'test-pipeline-id',
setShowInputFieldPanel: mockSetShowInputFieldPanel,
setShowEnvPanel: mockSetShowEnvPanel,
setShowImportDSLModal: mockSetShowImportDSLModal,
setIsPreparingDataSource: mockSetIsPreparingDataSource,
setShowDebugAndPreviewPanel: mockSetShowDebugAndPreviewPanel,
setPublishedAt: mockSetPublishedAt,

View File

@ -49,6 +49,11 @@ vi.mock('@/app/components/workflow/store', () => ({
}
return selector(state)
},
useWorkflowStore: () => ({
getState: () => ({
setRagPipelineVariables: mockSetRagPipelineVariables,
}),
}),
}))
// Mock useNodesSyncDraft hook

View File

@ -42,6 +42,8 @@ vi.mock('@/app/components/workflow/store', () => ({
useStore: (selector: (state: typeof mockStoreState) => unknown) => selector(mockStoreState),
useWorkflowStore: () => ({
getState: () => ({
setShowInputFieldPanel: mockSetShowInputFieldPanel,
setShowEnvPanel: mockSetShowEnvPanel,
setIsPreparingDataSource: mockSetIsPreparingDataSource,
setShowDebugAndPreviewPanel: mockSetShowDebugAndPreviewPanel,
setPublishedAt: mockSetPublishedAt,

View File

@ -1,7 +1,9 @@
import type { ReactElement } from 'react'
import type { AppPublisherProps } from '@/app/components/app/app-publisher'
import type { App } from '@/types/app'
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ToastContext } from '@/app/components/base/toast'
import { Plan } from '@/app/components/billing/type'
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
@ -17,7 +19,6 @@ const mockUseFeatures = vi.fn()
const mockUseProviderContext = vi.fn()
const mockUseNodes = vi.fn()
const mockUseEdges = vi.fn()
const mockUseAppStoreSelector = vi.fn()
const mockNotify = vi.fn()
const mockHandleCheckBeforePublish = vi.fn()
@ -27,7 +28,6 @@ const mockUpdatePublishedWorkflow = vi.fn()
const mockResetWorkflowVersionHistory = vi.fn()
const mockInvalidateAppTriggers = vi.fn()
const mockFetchAppDetail = vi.fn()
const mockSetAppDetail = vi.fn()
const mockSetPublishedAt = vi.fn()
const mockSetLastPublishedHasUserInput = vi.fn()
@ -134,9 +134,7 @@ vi.mock('@/hooks/use-theme', () => ({
default: () => mockUseTheme(),
}))
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: { appDetail?: { id: string }, setAppDetail: typeof mockSetAppDetail }) => unknown) => mockUseAppStoreSelector(selector),
}))
// Use real app store - global zustand mock will auto-reset between tests
const createProviderContext = ({
type = Plan.sandbox,
@ -178,7 +176,8 @@ describe('FeaturesTrigger', () => {
mockUseProviderContext.mockReturnValue(createProviderContext({}))
mockUseNodes.mockReturnValue([])
mockUseEdges.mockReturnValue([])
mockUseAppStoreSelector.mockImplementation(selector => selector({ appDetail: { id: 'app-id' }, setAppDetail: mockSetAppDetail }))
// Set up app store state
useAppStore.setState({ appDetail: { id: 'app-id' } as unknown as App })
mockFetchAppDetail.mockResolvedValue({ id: 'app-id' })
mockPublishWorkflow.mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
})
@ -424,7 +423,7 @@ describe('FeaturesTrigger', () => {
expect(mockResetWorkflowVersionHistory).toHaveBeenCalled()
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.api.actionSuccess' })
expect(mockFetchAppDetail).toHaveBeenCalledWith({ url: '/apps', id: 'app-id' })
expect(mockSetAppDetail).toHaveBeenCalled()
expect(useAppStore.getState().appDetail).toBeDefined()
})
})

View File

@ -1,12 +1,11 @@
import type { IChatItem } from '@/app/components/base/chat/chat/type'
import type { HeaderProps } from '@/app/components/workflow/header'
import type { App } from '@/types/app'
import { fireEvent, render, screen } from '@testing-library/react'
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { useStore as useAppStore } from '@/app/components/app/store'
import { AppModeEnum } from '@/types/app'
import WorkflowHeader from './index'
const mockUseAppStoreSelector = vi.fn()
const mockSetCurrentLogItem = vi.fn()
const mockSetShowMessageLogModal = vi.fn()
const mockResetWorkflowVersionHistory = vi.fn()
const createMockApp = (overrides: Partial<App> = {}): App => ({
@ -39,20 +38,14 @@ const createMockApp = (overrides: Partial<App> = {}): App => ({
...overrides,
})
let appDetail: App
const mockAppStore = (overrides: Partial<App> = {}) => {
appDetail = createMockApp(overrides)
mockUseAppStoreSelector.mockImplementation(selector => selector({
appDetail,
setCurrentLogItem: mockSetCurrentLogItem,
setShowMessageLogModal: mockSetShowMessageLogModal,
}))
// Helper to set up app store state
const setupAppStore = (overrides: Partial<App> = {}) => {
const appDetail = createMockApp(overrides)
useAppStore.setState({ appDetail })
return appDetail
}
vi.mock('@/app/components/app/store', () => ({
useStore: (selector: (state: { appDetail?: App, setCurrentLogItem: typeof mockSetCurrentLogItem, setShowMessageLogModal: typeof mockSetShowMessageLogModal }) => unknown) => mockUseAppStoreSelector(selector),
}))
// Use real store - global zustand mock will auto-reset between tests
vi.mock('@/app/components/workflow/header', () => ({
default: (props: HeaderProps) => {
@ -87,7 +80,12 @@ vi.mock('@/service/use-workflow', () => ({
describe('WorkflowHeader', () => {
beforeEach(() => {
vi.clearAllMocks()
mockAppStore()
setupAppStore()
})
afterEach(() => {
// Cleanup before zustand mock resets store to avoid re-render with undefined appDetail
cleanup()
})
// Verifies the wrapper renders the workflow header shell.
@ -105,7 +103,7 @@ describe('WorkflowHeader', () => {
describe('Props', () => {
it('should configure preview mode when app is in advanced chat mode', () => {
// Arrange
mockAppStore({ mode: AppModeEnum.ADVANCED_CHAT })
setupAppStore({ mode: AppModeEnum.ADVANCED_CHAT })
// Act
render(<WorkflowHeader />)
@ -119,7 +117,7 @@ describe('WorkflowHeader', () => {
it('should configure run mode when app is not in advanced chat mode', () => {
// Arrange
mockAppStore({ mode: AppModeEnum.COMPLETION })
setupAppStore({ mode: AppModeEnum.COMPLETION })
// Act
render(<WorkflowHeader />)
@ -136,14 +134,18 @@ describe('WorkflowHeader', () => {
describe('User Interactions', () => {
it('should clear log and close message modal when clearing history modal state', () => {
// Arrange
useAppStore.setState({
currentLogItem: { id: 'log-item' } as unknown as IChatItem,
showMessageLogModal: true,
})
render(<WorkflowHeader />)
// Act
fireEvent.click(screen.getByRole('button', { name: /clear-history/i }))
// Assert
expect(mockSetCurrentLogItem).toHaveBeenCalledWith()
expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(false)
// Assert - verify store state was updated
expect(useAppStore.getState().currentLogItem).toBeUndefined()
expect(useAppStore.getState().showMessageLogModal).toBe(false)
})
})

View File

@ -97,6 +97,8 @@ vi.mock('../../store', () => ({
useWorkflowStore: () => ({
getState: () => ({
deleteAllInspectVars: vi.fn(),
setShowWorkflowVersionHistoryPanel: vi.fn(),
setCurrentVersion: mockSetCurrentVersion,
}),
setState: vi.fn(),
}),