fix(web): Zustand testing best practices and state read optimization (#31163)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh
2026-01-19 10:31:34 +08:00
committed by GitHub
parent 8893913b3a
commit e8397ae7a8
18 changed files with 257 additions and 106 deletions

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)
})
})