mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 17:27:16 -05:00
chore(web): enhance tests follow the testing.md and skills (#29841)
This commit is contained in:
@@ -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(
|
||||
<AccessControlItem type={AccessMode.ORGANIZATION}>
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -39,13 +39,6 @@ jest.mock('@/app/components/header/account-setting/model-provider-page/model-par
|
||||
default: () => <div data-testid="model-parameter-modal" />,
|
||||
}))
|
||||
|
||||
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<typeof useModelListAndDefaultModelAndCurrentProviderAndModel>
|
||||
const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as jest.MockedFunction<typeof useCurrentProviderAndModel>
|
||||
|
||||
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<DatasetConfigs> = {}): 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',
|
||||
})
|
||||
|
||||
@@ -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 <div role="dialog">{children}</div>
|
||||
}
|
||||
|
||||
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<typeof useModelListAndDefaultModelAndCurrentProviderAndModel>
|
||||
const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as jest.MockedFunction<typeof useCurrentProviderAndModel>
|
||||
const mockToastNotify = Toast.notify as unknown as jest.Mock
|
||||
let toastNotifySpy: jest.SpyInstance
|
||||
|
||||
const createDatasetConfigs = (overrides: Partial<DatasetConfigs> = {}): 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',
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('ChatVariableTrigger', () => {
|
||||
render(<ChatVariableTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('chat-variable-button')).not.toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'ChatVariableButton' })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -54,7 +54,7 @@ describe('ChatVariableTrigger', () => {
|
||||
render(<ChatVariableTrigger />)
|
||||
|
||||
// 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(<ChatVariableTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('chat-variable-button')).toBeDisabled()
|
||||
expect(screen.getByRole('button', { name: 'ChatVariableButton' })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, unknown> | 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<string, unknown>) => 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<string, unknown>) => {
|
||||
capturedAppPublisherProps = props
|
||||
default: (props: AppPublisherProps) => {
|
||||
const inputs = props.inputs ?? []
|
||||
return (
|
||||
<div
|
||||
data-testid='app-publisher'
|
||||
data-disabled={String(Boolean(props.disabled))}
|
||||
data-publish-disabled={String(Boolean(props.publishDisabled))}
|
||||
/>
|
||||
data-start-node-limit-exceeded={String(Boolean(props.startNodeLimitExceeded))}
|
||||
data-has-trigger-node={String(Boolean(props.hasTriggerNode))}
|
||||
data-inputs={JSON.stringify(inputs)}
|
||||
>
|
||||
<button type="button" onClick={() => { props.onRefreshData?.() }}>
|
||||
publisher-refresh
|
||||
</button>
|
||||
<button type="button" onClick={() => { props.onToggle?.(true) }}>
|
||||
publisher-toggle-on
|
||||
</button>
|
||||
<button type="button" onClick={() => { props.onToggle?.(false) }}>
|
||||
publisher-toggle-off
|
||||
</button>
|
||||
<button type="button" onClick={() => { Promise.resolve(props.onPublish?.()).catch(() => undefined) }}>
|
||||
publisher-publish
|
||||
</button>
|
||||
<button type="button" onClick={() => { Promise.resolve(props.onPublish?.({ title: 'Test title', releaseNotes: 'Test notes' })).catch(() => undefined) }}>
|
||||
publisher-publish-with-params
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
@@ -147,10 +161,17 @@ const createProviderContext = ({
|
||||
isFetchedPlan,
|
||||
})
|
||||
|
||||
const renderWithToast = (ui: ReactElement) => {
|
||||
return render(
|
||||
<ToastContext.Provider value={{ notify: mockNotify, close: jest.fn() }}>
|
||||
{ui}
|
||||
</ToastContext.Provider>,
|
||||
)
|
||||
}
|
||||
|
||||
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<string, unknown>) => unknown) => selector({ features: { file: {} } }))
|
||||
mockUseProviderContext.mockReturnValue(createProviderContext({}))
|
||||
mockUseNodes.mockReturnValue([])
|
||||
@@ -182,7 +202,7 @@ describe('FeaturesTrigger', () => {
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByRole('button', { name: /workflow\.common\.features/i })).not.toBeInTheDocument()
|
||||
@@ -193,7 +213,7 @@ describe('FeaturesTrigger', () => {
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /workflow\.common\.features/i })).toBeInTheDocument()
|
||||
@@ -205,7 +225,7 @@ describe('FeaturesTrigger', () => {
|
||||
mockUseTheme.mockReturnValue({ theme: 'dark' })
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// 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(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /workflow\.common\.features/i }))
|
||||
@@ -242,7 +262,7 @@ describe('FeaturesTrigger', () => {
|
||||
isRestoring: false,
|
||||
}
|
||||
|
||||
render(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /workflow\.common\.features/i }))
|
||||
@@ -260,10 +280,9 @@ describe('FeaturesTrigger', () => {
|
||||
mockUseNodes.mockReturnValue([])
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(capturedAppPublisherProps?.disabled).toBe(true)
|
||||
expect(screen.getByTestId('app-publisher')).toHaveAttribute('data-disabled', 'true')
|
||||
})
|
||||
})
|
||||
@@ -280,10 +299,15 @@ describe('FeaturesTrigger', () => {
|
||||
])
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// 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(<FeaturesTrigger />)
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// 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(<FeaturesTrigger />)
|
||||
const refresh = capturedAppPublisherProps?.onRefreshData as unknown as (() => void) | undefined
|
||||
expect(refresh).toBeDefined()
|
||||
const user = userEvent.setup()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// 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(<FeaturesTrigger />)
|
||||
const onToggle = capturedAppPublisherProps?.onToggle as unknown as ((state: boolean) => void) | undefined
|
||||
expect(onToggle).toBeDefined()
|
||||
const user = userEvent.setup()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// 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(<FeaturesTrigger />)
|
||||
const onToggle = capturedAppPublisherProps?.onToggle as unknown as ((state: boolean) => void) | undefined
|
||||
expect(onToggle).toBeDefined()
|
||||
const user = userEvent.setup()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
onToggle?.(false)
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-toggle-off' }))
|
||||
|
||||
// Assert
|
||||
expect(mockHandleSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
@@ -357,48 +379,51 @@ 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(<FeaturesTrigger />)
|
||||
|
||||
const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise<void>) | undefined
|
||||
expect(onPublish).toBeDefined()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await expect(onPublish?.()).rejects.toThrow('Checklist has unresolved items')
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish' }))
|
||||
|
||||
// Assert
|
||||
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(<FeaturesTrigger />)
|
||||
|
||||
const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise<void>) | undefined
|
||||
expect(onPublish).toBeDefined()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// 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(<FeaturesTrigger />)
|
||||
|
||||
const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise<void>) | undefined
|
||||
expect(onPublish).toBeDefined()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await onPublish?.()
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish' }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockPublishWorkflow).toHaveBeenCalledWith({
|
||||
url: '/apps/app-id/workflows/publish',
|
||||
title: '',
|
||||
@@ -410,8 +435,6 @@ describe('FeaturesTrigger', () => {
|
||||
expect(mockSetLastPublishedHasUserInput).toHaveBeenCalledWith(true)
|
||||
expect(mockResetWorkflowVersionHistory).toHaveBeenCalled()
|
||||
expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'common.api.actionSuccess' })
|
||||
|
||||
await waitFor(() => {
|
||||
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(<FeaturesTrigger />)
|
||||
|
||||
const onPublish = capturedAppPublisherProps?.onPublish as unknown as ((params: { title: string; releaseNotes: string }) => Promise<void>) | undefined
|
||||
expect(onPublish).toBeDefined()
|
||||
const user = userEvent.setup()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await onPublish?.({ title: 'Test title', releaseNotes: 'Test notes' })
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish-with-params' }))
|
||||
|
||||
// Assert
|
||||
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(<FeaturesTrigger />)
|
||||
|
||||
const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise<void>) | undefined
|
||||
expect(onPublish).toBeDefined()
|
||||
renderWithToast(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await onPublish?.()
|
||||
await user.click(screen.getByRole('button', { name: 'publisher-publish' }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -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 <div data-testid='workflow-header' />
|
||||
const historyFetcher = props.normal?.runAndHistoryProps?.viewHistoryProps?.historyFetcher
|
||||
const hasHistoryFetcher = typeof historyFetcher === 'function'
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid='workflow-header'
|
||||
data-show-run={String(Boolean(props.normal?.runAndHistoryProps?.showRunButton))}
|
||||
data-show-preview={String(Boolean(props.normal?.runAndHistoryProps?.showPreviewButton))}
|
||||
data-history-url={props.normal?.runAndHistoryProps?.viewHistoryProps?.historyUrl ?? ''}
|
||||
data-has-history-fetcher={String(hasHistoryFetcher)}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.normal?.runAndHistoryProps?.viewHistoryProps?.onClearLogAndMessageModal?.()}
|
||||
>
|
||||
clear-history
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => props.restoring?.onRestoreSettled?.()}
|
||||
>
|
||||
restore-settled
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -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(<WorkflowHeader />)
|
||||
|
||||
// Assert
|
||||
expect(capturedHeaderProps).not.toBeNull()
|
||||
expect(screen.getByTestId('workflow-header')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -93,10 +113,11 @@ describe('WorkflowHeader', () => {
|
||||
render(<WorkflowHeader />)
|
||||
|
||||
// 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(<WorkflowHeader />)
|
||||
|
||||
// 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(<WorkflowHeader />)
|
||||
|
||||
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(<WorkflowHeader />)
|
||||
|
||||
// Assert
|
||||
expect(capturedHeaderProps?.restoring?.onRestoreSettled).toBe(mockResetWorkflowVersionHistory)
|
||||
screen.getByRole('button', { name: 'restore-settled' }).click()
|
||||
expect(mockResetWorkflowVersionHistory).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user