From 98b1ec0d2978317f0ad3ed9001e8607c996602e5 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:54:00 +0800 Subject: [PATCH] chore(web): enhance tests follow the testing.md and skills (#29841) --- .../access-control.spec.tsx | 7 +- .../params-config/config-content.spec.tsx | 18 +- .../params-config/index.spec.tsx | 76 +++---- .../chat-variable-trigger.spec.tsx | 6 +- .../workflow-header/features-trigger.spec.tsx | 187 ++++++++++-------- .../components/workflow-header/index.spec.tsx | 59 ++++-- 6 files changed, 190 insertions(+), 163 deletions(-) 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 2959500a29..ea0e17de2e 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 @@ -181,7 +181,7 @@ describe('AccessControlItem', () => { expect(useAccessControlStore.getState().currentMenu).toBe(AccessMode.ORGANIZATION) }) - it('should render selected styles when the current menu matches the type', () => { + it('should keep current menu when clicking the selected access type', () => { useAccessControlStore.setState({ currentMenu: AccessMode.ORGANIZATION }) render( @@ -190,8 +190,9 @@ describe('AccessControlItem', () => { ) const option = screen.getByText('Organization Only').parentElement as HTMLElement - expect(option.className).toContain('border-[1.5px]') - expect(option.className).not.toContain('cursor-pointer') + fireEvent.click(option) + + expect(useAccessControlStore.getState().currentMenu).toBe(AccessMode.ORGANIZATION) }) }) diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx index a7673a7491..e44eba6c03 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.spec.tsx @@ -39,13 +39,6 @@ jest.mock('@/app/components/header/account-setting/model-provider-page/model-par default: () =>
, })) -jest.mock('@/app/components/base/toast', () => ({ - __esModule: true, - default: { - notify: jest.fn(), - }, -})) - jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useModelListAndDefaultModelAndCurrentProviderAndModel: jest.fn(), useCurrentProviderAndModel: jest.fn(), @@ -54,7 +47,7 @@ jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', ( const mockedUseModelListAndDefaultModelAndCurrentProviderAndModel = useModelListAndDefaultModelAndCurrentProviderAndModel as jest.MockedFunction const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as jest.MockedFunction -const mockToastNotify = Toast.notify as unknown as jest.Mock +let toastNotifySpy: jest.SpyInstance const baseRetrievalConfig: RetrievalConfig = { search_method: RETRIEVE_METHOD.semantic, @@ -180,6 +173,7 @@ const createDatasetConfigs = (overrides: Partial = {}): DatasetC describe('ConfigContent', () => { beforeEach(() => { jest.clearAllMocks() + toastNotifySpy = jest.spyOn(Toast, 'notify').mockImplementation(() => ({})) mockedUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({ modelList: [], defaultModel: undefined, @@ -192,6 +186,10 @@ describe('ConfigContent', () => { }) }) + afterEach(() => { + toastNotifySpy.mockRestore() + }) + // State management describe('Effects', () => { it('should normalize oneWay retrieval mode to multiWay', async () => { @@ -336,7 +334,7 @@ describe('ConfigContent', () => { await user.click(screen.getByText('common.modelProvider.rerankModel.key')) // Assert - expect(mockToastNotify).toHaveBeenCalledWith({ + expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'error', message: 'workflow.errorMsg.rerankModelRequired', }) @@ -378,7 +376,7 @@ describe('ConfigContent', () => { await user.click(screen.getByRole('switch')) // Assert - expect(mockToastNotify).toHaveBeenCalledWith({ + expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'error', message: 'workflow.errorMsg.rerankModelRequired', }) diff --git a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx index 3303c484a1..b666a6cb5b 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { render, screen, waitFor } from '@testing-library/react' -import userEvent from '@testing-library/user-event' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import ParamsConfig from './index' import ConfigContext from '@/context/debug-configuration' import type { DatasetConfigs } from '@/models/debug' @@ -12,30 +11,6 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel, } from '@/app/components/header/account-setting/model-provider-page/hooks' -jest.mock('@/app/components/base/modal', () => { - type Props = { - isShow: boolean - children?: React.ReactNode - } - - const MockModal = ({ isShow, children }: Props) => { - if (!isShow) return null - return
{children}
- } - - return { - __esModule: true, - default: MockModal, - } -}) - -jest.mock('@/app/components/base/toast', () => ({ - __esModule: true, - default: { - notify: jest.fn(), - }, -})) - jest.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({ useModelListAndDefaultModelAndCurrentProviderAndModel: jest.fn(), useCurrentProviderAndModel: jest.fn(), @@ -69,7 +44,7 @@ jest.mock('@/app/components/header/account-setting/model-provider-page/model-par const mockedUseModelListAndDefaultModelAndCurrentProviderAndModel = useModelListAndDefaultModelAndCurrentProviderAndModel as jest.MockedFunction const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as jest.MockedFunction -const mockToastNotify = Toast.notify as unknown as jest.Mock +let toastNotifySpy: jest.SpyInstance const createDatasetConfigs = (overrides: Partial = {}): DatasetConfigs => { return { @@ -143,6 +118,8 @@ const renderParamsConfig = ({ describe('dataset-config/params-config', () => { beforeEach(() => { jest.clearAllMocks() + jest.useRealTimers() + toastNotifySpy = jest.spyOn(Toast, 'notify').mockImplementation(() => ({})) mockedUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({ modelList: [], defaultModel: undefined, @@ -155,6 +132,10 @@ describe('dataset-config/params-config', () => { }) }) + afterEach(() => { + toastNotifySpy.mockRestore() + }) + // Rendering tests (REQUIRED) describe('Rendering', () => { it('should disable settings trigger when disabled is true', () => { @@ -170,18 +151,19 @@ describe('dataset-config/params-config', () => { describe('User Interactions', () => { it('should open modal and persist changes when save is clicked', async () => { // Arrange - const user = userEvent.setup() const { setDatasetConfigsSpy } = renderParamsConfig() // Act - await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) - await screen.findByRole('dialog') + fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) + const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) + const dialogScope = within(dialog) // Change top_k via the first number input increment control. - const incrementButtons = screen.getAllByRole('button', { name: 'increment' }) - await user.click(incrementButtons[0]) + const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' }) + fireEvent.click(incrementButtons[0]) - await user.click(screen.getByRole('button', { name: 'common.operation.save' })) + const saveButton = await dialogScope.findByRole('button', { name: 'common.operation.save' }) + fireEvent.click(saveButton) // Assert expect(setDatasetConfigsSpy).toHaveBeenCalledWith(expect.objectContaining({ top_k: 5 })) @@ -192,25 +174,28 @@ describe('dataset-config/params-config', () => { it('should discard changes when cancel is clicked', async () => { // Arrange - const user = userEvent.setup() const { setDatasetConfigsSpy } = renderParamsConfig() // Act - await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) - await screen.findByRole('dialog') + fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) + const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) + const dialogScope = within(dialog) - const incrementButtons = screen.getAllByRole('button', { name: 'increment' }) - await user.click(incrementButtons[0]) + const incrementButtons = dialogScope.getAllByRole('button', { name: 'increment' }) + fireEvent.click(incrementButtons[0]) - await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + const cancelButton = await dialogScope.findByRole('button', { name: 'common.operation.cancel' }) + fireEvent.click(cancelButton) await waitFor(() => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument() }) // Re-open and save without changes. - await user.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) - await screen.findByRole('dialog') - await user.click(screen.getByRole('button', { name: 'common.operation.save' })) + fireEvent.click(screen.getByRole('button', { name: 'dataset.retrievalSettings' })) + const reopenedDialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) + const reopenedScope = within(reopenedDialog) + const reopenedSave = await reopenedScope.findByRole('button', { name: 'common.operation.save' }) + fireEvent.click(reopenedSave) // Assert - should save original top_k rather than the canceled change. expect(setDatasetConfigsSpy).toHaveBeenCalledWith(expect.objectContaining({ top_k: 4 })) @@ -218,7 +203,6 @@ describe('dataset-config/params-config', () => { it('should prevent saving when rerank model is required but invalid', async () => { // Arrange - const user = userEvent.setup() const { setDatasetConfigsSpy } = renderParamsConfig({ datasetConfigs: createDatasetConfigs({ reranking_enable: true, @@ -228,10 +212,12 @@ describe('dataset-config/params-config', () => { }) // Act - await user.click(screen.getByRole('button', { name: 'common.operation.save' })) + const dialog = await screen.findByRole('dialog', {}, { timeout: 3000 }) + const dialogScope = within(dialog) + fireEvent.click(dialogScope.getByRole('button', { name: 'common.operation.save' })) // Assert - expect(mockToastNotify).toHaveBeenCalledWith({ + expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'error', message: 'appDebug.datasetConfig.rerankModelRequired', }) diff --git a/web/app/components/workflow-app/components/workflow-header/chat-variable-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/chat-variable-trigger.spec.tsx index 39c0b83d07..fa9d8e437c 100644 --- a/web/app/components/workflow-app/components/workflow-header/chat-variable-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/chat-variable-trigger.spec.tsx @@ -39,7 +39,7 @@ describe('ChatVariableTrigger', () => { render() // Assert - expect(screen.queryByTestId('chat-variable-button')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'ChatVariableButton' })).not.toBeInTheDocument() }) }) @@ -54,7 +54,7 @@ describe('ChatVariableTrigger', () => { render() // Assert - expect(screen.getByTestId('chat-variable-button')).toBeEnabled() + expect(screen.getByRole('button', { name: 'ChatVariableButton' })).toBeEnabled() }) it('should render disabled ChatVariableButton when nodes are read-only', () => { @@ -66,7 +66,7 @@ describe('ChatVariableTrigger', () => { render() // Assert - expect(screen.getByTestId('chat-variable-button')).toBeDisabled() + expect(screen.getByRole('button', { name: 'ChatVariableButton' })).toBeDisabled() }) }) }) diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx index a3fc2c12a9..5e21e54fb3 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.spec.tsx @@ -1,6 +1,9 @@ +import type { ReactElement } from 'react' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { Plan } from '@/app/components/billing/type' +import type { AppPublisherProps } from '@/app/components/app/app-publisher' +import { ToastContext } from '@/app/components/base/toast' import { BlockEnum, InputVarType } from '@/app/components/workflow/types' import FeaturesTrigger from './features-trigger' @@ -10,7 +13,6 @@ const mockUseNodesReadOnly = jest.fn() const mockUseChecklist = jest.fn() const mockUseChecklistBeforePublish = jest.fn() const mockUseNodesSyncDraft = jest.fn() -const mockUseToastContext = jest.fn() const mockUseFeatures = jest.fn() const mockUseProviderContext = jest.fn() const mockUseNodes = jest.fn() @@ -45,8 +47,6 @@ const mockWorkflowStore = { setState: mockWorkflowStoreSetState, } -let capturedAppPublisherProps: Record | null = null - jest.mock('@/app/components/workflow/hooks', () => ({ __esModule: true, useChecklist: (...args: unknown[]) => mockUseChecklist(...args), @@ -75,11 +75,6 @@ jest.mock('@/app/components/base/features/hooks', () => ({ useFeatures: (selector: (state: Record) => unknown) => mockUseFeatures(selector), })) -jest.mock('@/app/components/base/toast', () => ({ - __esModule: true, - useToastContext: () => mockUseToastContext(), -})) - jest.mock('@/context/provider-context', () => ({ __esModule: true, useProviderContext: () => mockUseProviderContext(), @@ -97,14 +92,33 @@ jest.mock('reactflow', () => ({ jest.mock('@/app/components/app/app-publisher', () => ({ __esModule: true, - default: (props: Record) => { - capturedAppPublisherProps = props + default: (props: AppPublisherProps) => { + const inputs = props.inputs ?? [] return (
+ data-start-node-limit-exceeded={String(Boolean(props.startNodeLimitExceeded))} + data-has-trigger-node={String(Boolean(props.hasTriggerNode))} + data-inputs={JSON.stringify(inputs)} + > + + + + + +
) }, })) @@ -147,10 +161,17 @@ const createProviderContext = ({ isFetchedPlan, }) +const renderWithToast = (ui: ReactElement) => { + return render( + + {ui} + , + ) +} + describe('FeaturesTrigger', () => { beforeEach(() => { jest.clearAllMocks() - capturedAppPublisherProps = null workflowStoreState = { showFeaturesPanel: false, isRestoring: false, @@ -165,7 +186,6 @@ describe('FeaturesTrigger', () => { mockUseChecklistBeforePublish.mockReturnValue({ handleCheckBeforePublish: mockHandleCheckBeforePublish }) mockHandleCheckBeforePublish.mockResolvedValue(true) mockUseNodesSyncDraft.mockReturnValue({ handleSyncWorkflowDraft: mockHandleSyncWorkflowDraft }) - mockUseToastContext.mockReturnValue({ notify: mockNotify }) mockUseFeatures.mockImplementation((selector: (state: Record) => unknown) => selector({ features: { file: {} } })) mockUseProviderContext.mockReturnValue(createProviderContext({})) mockUseNodes.mockReturnValue([]) @@ -182,7 +202,7 @@ describe('FeaturesTrigger', () => { mockUseIsChatMode.mockReturnValue(false) // Act - render() + renderWithToast() // Assert expect(screen.queryByRole('button', { name: /workflow\.common\.features/i })).not.toBeInTheDocument() @@ -193,7 +213,7 @@ describe('FeaturesTrigger', () => { mockUseIsChatMode.mockReturnValue(true) // Act - render() + renderWithToast() // Assert expect(screen.getByRole('button', { name: /workflow\.common\.features/i })).toBeInTheDocument() @@ -205,7 +225,7 @@ describe('FeaturesTrigger', () => { mockUseTheme.mockReturnValue({ theme: 'dark' }) // Act - render() + renderWithToast() // Assert expect(screen.getByRole('button', { name: /workflow\.common\.features/i })).toHaveClass('rounded-lg') @@ -220,7 +240,7 @@ describe('FeaturesTrigger', () => { mockUseIsChatMode.mockReturnValue(true) mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false, getNodesReadOnly: () => false }) - render() + renderWithToast() // Act await user.click(screen.getByRole('button', { name: /workflow\.common\.features/i })) @@ -242,7 +262,7 @@ describe('FeaturesTrigger', () => { isRestoring: false, } - render() + renderWithToast() // Act await user.click(screen.getByRole('button', { name: /workflow\.common\.features/i })) @@ -260,10 +280,9 @@ describe('FeaturesTrigger', () => { mockUseNodes.mockReturnValue([]) // Act - render() + renderWithToast() // Assert - expect(capturedAppPublisherProps?.disabled).toBe(true) expect(screen.getByTestId('app-publisher')).toHaveAttribute('data-disabled', 'true') }) }) @@ -280,10 +299,15 @@ describe('FeaturesTrigger', () => { ]) // Act - render() + renderWithToast() // Assert - const inputs = (capturedAppPublisherProps?.inputs as unknown as Array<{ type?: string; variable?: string }>) || [] + const inputs = JSON.parse(screen.getByTestId('app-publisher').getAttribute('data-inputs') ?? '[]') as Array<{ + type?: string + variable?: string + required?: boolean + label?: string + }> expect(inputs).toContainEqual({ type: InputVarType.files, variable: '__image', @@ -302,51 +326,49 @@ describe('FeaturesTrigger', () => { ]) // Act - render() + renderWithToast() // Assert - expect(capturedAppPublisherProps?.startNodeLimitExceeded).toBe(true) - expect(capturedAppPublisherProps?.publishDisabled).toBe(true) - expect(capturedAppPublisherProps?.hasTriggerNode).toBe(true) + const publisher = screen.getByTestId('app-publisher') + expect(publisher).toHaveAttribute('data-start-node-limit-exceeded', 'true') + expect(publisher).toHaveAttribute('data-publish-disabled', 'true') + expect(publisher).toHaveAttribute('data-has-trigger-node', 'true') }) }) // Verifies callbacks wired from AppPublisher to stores and draft syncing. describe('Callbacks', () => { - it('should set toolPublished when AppPublisher refreshes data', () => { + it('should set toolPublished when AppPublisher refreshes data', async () => { // Arrange - render() - const refresh = capturedAppPublisherProps?.onRefreshData as unknown as (() => void) | undefined - expect(refresh).toBeDefined() + const user = userEvent.setup() + renderWithToast() // Act - refresh?.() + await user.click(screen.getByRole('button', { name: 'publisher-refresh' })) // Assert expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ toolPublished: true }) }) - it('should sync workflow draft when AppPublisher toggles on', () => { + it('should sync workflow draft when AppPublisher toggles on', async () => { // Arrange - render() - const onToggle = capturedAppPublisherProps?.onToggle as unknown as ((state: boolean) => void) | undefined - expect(onToggle).toBeDefined() + const user = userEvent.setup() + renderWithToast() // Act - onToggle?.(true) + await user.click(screen.getByRole('button', { name: 'publisher-toggle-on' })) // Assert expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true) }) - it('should not sync workflow draft when AppPublisher toggles off', () => { + it('should not sync workflow draft when AppPublisher toggles off', async () => { // Arrange - render() - const onToggle = capturedAppPublisherProps?.onToggle as unknown as ((state: boolean) => void) | undefined - expect(onToggle).toBeDefined() + const user = userEvent.setup() + renderWithToast() // Act - onToggle?.(false) + await user.click(screen.getByRole('button', { name: 'publisher-toggle-off' })) // Assert expect(mockHandleSyncWorkflowDraft).not.toHaveBeenCalled() @@ -357,61 +379,62 @@ describe('FeaturesTrigger', () => { describe('Publishing', () => { it('should notify error and reject publish when checklist has warning nodes', async () => { // Arrange + const user = userEvent.setup() mockUseChecklist.mockReturnValue([{ id: 'warning' }]) - render() - - const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise) | undefined - expect(onPublish).toBeDefined() + renderWithToast() // Act - await expect(onPublish?.()).rejects.toThrow('Checklist has unresolved items') + await user.click(screen.getByRole('button', { name: 'publisher-publish' })) // Assert - expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'workflow.panel.checklistTip' }) + await waitFor(() => { + expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'workflow.panel.checklistTip' }) + }) + expect(mockPublishWorkflow).not.toHaveBeenCalled() }) it('should reject publish when checklist before publish fails', async () => { // Arrange + const user = userEvent.setup() mockHandleCheckBeforePublish.mockResolvedValue(false) - render() - - const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise) | undefined - expect(onPublish).toBeDefined() + renderWithToast() // Act & Assert - await expect(onPublish?.()).rejects.toThrow('Checklist failed') + await user.click(screen.getByRole('button', { name: 'publisher-publish' })) + + await waitFor(() => { + expect(mockHandleCheckBeforePublish).toHaveBeenCalled() + }) + expect(mockPublishWorkflow).not.toHaveBeenCalled() }) it('should publish workflow and update related stores when validation passes', async () => { // Arrange + const user = userEvent.setup() mockUseNodes.mockReturnValue([ { id: 'start', data: { type: BlockEnum.Start } }, ]) mockUseEdges.mockReturnValue([ { source: 'start' }, ]) - render() - - const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise) | undefined - expect(onPublish).toBeDefined() + renderWithToast() // Act - await onPublish?.() + await user.click(screen.getByRole('button', { name: 'publisher-publish' })) // Assert - expect(mockPublishWorkflow).toHaveBeenCalledWith({ - url: '/apps/app-id/workflows/publish', - title: '', - releaseNotes: '', - }) - expect(mockUpdatePublishedWorkflow).toHaveBeenCalledWith('app-id') - expect(mockInvalidateAppTriggers).toHaveBeenCalledWith('app-id') - expect(mockSetPublishedAt).toHaveBeenCalledWith('2024-01-01T00:00:00Z') - expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true) - expect(mockResetWorkflowVersionHistory).toHaveBeenCalled() - expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.api.actionSuccess' }) - await waitFor(() => { + expect(mockPublishWorkflow).toHaveBeenCalledWith({ + url: '/apps/app-id/workflows/publish', + title: '', + releaseNotes: '', + }) + expect(mockUpdatePublishedWorkflow).toHaveBeenCalledWith('app-id') + expect(mockInvalidateAppTriggers).toHaveBeenCalledWith('app-id') + expect(mockSetPublishedAt).toHaveBeenCalledWith('2024-01-01T00:00:00Z') + expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true) + expect(mockResetWorkflowVersionHistory).toHaveBeenCalled() + expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.api.actionSuccess' }) expect(mockFetchAppDetail).toHaveBeenCalledWith({ url: '/apps', id: 'app-id' }) expect(mockSetAppDetail).toHaveBeenCalled() }) @@ -419,34 +442,32 @@ describe('FeaturesTrigger', () => { it('should pass publish params to workflow publish mutation', async () => { // Arrange - render() - - const onPublish = capturedAppPublisherProps?.onPublish as unknown as ((params: { title: string; releaseNotes: string }) => Promise) | undefined - expect(onPublish).toBeDefined() + const user = userEvent.setup() + renderWithToast() // Act - await onPublish?.({ title: 'Test title', releaseNotes: 'Test notes' }) + await user.click(screen.getByRole('button', { name: 'publisher-publish-with-params' })) // Assert - expect(mockPublishWorkflow).toHaveBeenCalledWith({ - url: '/apps/app-id/workflows/publish', - title: 'Test title', - releaseNotes: 'Test notes', + await waitFor(() => { + expect(mockPublishWorkflow).toHaveBeenCalledWith({ + url: '/apps/app-id/workflows/publish', + title: 'Test title', + releaseNotes: 'Test notes', + }) }) }) it('should log error when app detail refresh fails after publish', async () => { // Arrange + const user = userEvent.setup() const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined) mockFetchAppDetail.mockRejectedValue(new Error('fetch failed')) - render() - - const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise) | undefined - expect(onPublish).toBeDefined() + renderWithToast() // Act - await onPublish?.() + await user.click(screen.getByRole('button', { name: 'publisher-publish' })) // Assert await waitFor(() => { diff --git a/web/app/components/workflow-app/components/workflow-header/index.spec.tsx b/web/app/components/workflow-app/components/workflow-header/index.spec.tsx index 4dd90610bf..cbeecaf26f 100644 --- a/web/app/components/workflow-app/components/workflow-header/index.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/index.spec.tsx @@ -1,16 +1,14 @@ -import { render } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import type { App } from '@/types/app' import { AppModeEnum } from '@/types/app' import type { HeaderProps } from '@/app/components/workflow/header' import WorkflowHeader from './index' -import { fetchWorkflowRunHistory } from '@/service/workflow' const mockUseAppStoreSelector = jest.fn() const mockSetCurrentLogItem = jest.fn() const mockSetShowMessageLogModal = jest.fn() const mockResetWorkflowVersionHistory = jest.fn() -let capturedHeaderProps: HeaderProps | null = null let appDetail: App jest.mock('ky', () => ({ @@ -39,8 +37,31 @@ jest.mock('@/app/components/app/store', () => ({ jest.mock('@/app/components/workflow/header', () => ({ __esModule: true, default: (props: HeaderProps) => { - capturedHeaderProps = props - return
+ const historyFetcher = props.normal?.runAndHistoryProps?.viewHistoryProps?.historyFetcher + const hasHistoryFetcher = typeof historyFetcher === 'function' + + return ( +
+ + +
+ ) }, })) @@ -57,7 +78,6 @@ jest.mock('@/service/use-workflow', () => ({ describe('WorkflowHeader', () => { beforeEach(() => { jest.clearAllMocks() - capturedHeaderProps = null appDetail = { id: 'app-id', mode: AppModeEnum.COMPLETION } as unknown as App mockUseAppStoreSelector.mockImplementation(selector => selector({ @@ -74,7 +94,7 @@ describe('WorkflowHeader', () => { render() // Assert - expect(capturedHeaderProps).not.toBeNull() + expect(screen.getByTestId('workflow-header')).toBeInTheDocument() }) }) @@ -93,10 +113,11 @@ describe('WorkflowHeader', () => { render() // Assert - expect(capturedHeaderProps?.normal?.runAndHistoryProps?.showRunButton).toBe(false) - expect(capturedHeaderProps?.normal?.runAndHistoryProps?.showPreviewButton).toBe(true) - expect(capturedHeaderProps?.normal?.runAndHistoryProps?.viewHistoryProps?.historyUrl).toBe('/apps/app-id/advanced-chat/workflow-runs') - expect(capturedHeaderProps?.normal?.runAndHistoryProps?.viewHistoryProps?.historyFetcher).toBe(fetchWorkflowRunHistory) + const header = screen.getByTestId('workflow-header') + expect(header).toHaveAttribute('data-show-run', 'false') + expect(header).toHaveAttribute('data-show-preview', 'true') + expect(header).toHaveAttribute('data-history-url', '/apps/app-id/advanced-chat/workflow-runs') + expect(header).toHaveAttribute('data-has-history-fetcher', 'true') }) it('should configure run mode when app is not in advanced chat mode', () => { @@ -112,9 +133,11 @@ describe('WorkflowHeader', () => { render() // Assert - expect(capturedHeaderProps?.normal?.runAndHistoryProps?.showRunButton).toBe(true) - expect(capturedHeaderProps?.normal?.runAndHistoryProps?.showPreviewButton).toBe(false) - expect(capturedHeaderProps?.normal?.runAndHistoryProps?.viewHistoryProps?.historyUrl).toBe('/apps/app-id/workflow-runs') + const header = screen.getByTestId('workflow-header') + expect(header).toHaveAttribute('data-show-run', 'true') + expect(header).toHaveAttribute('data-show-preview', 'false') + expect(header).toHaveAttribute('data-history-url', '/apps/app-id/workflow-runs') + expect(header).toHaveAttribute('data-has-history-fetcher', 'true') }) }) @@ -124,11 +147,8 @@ describe('WorkflowHeader', () => { // Arrange render() - const clear = capturedHeaderProps?.normal?.runAndHistoryProps?.viewHistoryProps?.onClearLogAndMessageModal - expect(clear).toBeDefined() - // Act - clear?.() + screen.getByRole('button', { name: 'clear-history' }).click() // Assert expect(mockSetCurrentLogItem).toHaveBeenCalledWith() @@ -143,7 +163,8 @@ describe('WorkflowHeader', () => { render() // Assert - expect(capturedHeaderProps?.restoring?.onRestoreSettled).toBe(mockResetWorkflowVersionHistory) + screen.getByRole('button', { name: 'restore-settled' }).click() + expect(mockResetWorkflowVersionHistory).toHaveBeenCalled() }) }) })