chore: add some jest tests (#29800)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import ChatVariableTrigger from './chat-variable-trigger'
|
||||
|
||||
const mockUseNodesReadOnly = jest.fn()
|
||||
const mockUseIsChatMode = jest.fn()
|
||||
|
||||
jest.mock('@/app/components/workflow/hooks', () => ({
|
||||
__esModule: true,
|
||||
useNodesReadOnly: () => mockUseNodesReadOnly(),
|
||||
}))
|
||||
|
||||
jest.mock('../../hooks', () => ({
|
||||
__esModule: true,
|
||||
useIsChatMode: () => mockUseIsChatMode(),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/header/chat-variable-button', () => ({
|
||||
__esModule: true,
|
||||
default: ({ disabled }: { disabled: boolean }) => (
|
||||
<button data-testid='chat-variable-button' type='button' disabled={disabled}>
|
||||
ChatVariableButton
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('ChatVariableTrigger', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
// Verifies conditional rendering when chat mode is off.
|
||||
describe('Rendering', () => {
|
||||
it('should not render when not in chat mode', () => {
|
||||
// Arrange
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false })
|
||||
|
||||
// Act
|
||||
render(<ChatVariableTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('chat-variable-button')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies the disabled state reflects read-only nodes.
|
||||
describe('Props', () => {
|
||||
it('should render enabled ChatVariableButton when nodes are editable', () => {
|
||||
// Arrange
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false })
|
||||
|
||||
// Act
|
||||
render(<ChatVariableTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('chat-variable-button')).toBeEnabled()
|
||||
})
|
||||
|
||||
it('should render disabled ChatVariableButton when nodes are read-only', () => {
|
||||
// Arrange
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: true })
|
||||
|
||||
// Act
|
||||
render(<ChatVariableTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('chat-variable-button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,458 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { BlockEnum, InputVarType } from '@/app/components/workflow/types'
|
||||
import FeaturesTrigger from './features-trigger'
|
||||
|
||||
const mockUseIsChatMode = jest.fn()
|
||||
const mockUseTheme = jest.fn()
|
||||
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()
|
||||
const mockUseEdges = jest.fn()
|
||||
const mockUseAppStoreSelector = jest.fn()
|
||||
|
||||
const mockNotify = jest.fn()
|
||||
const mockHandleCheckBeforePublish = jest.fn()
|
||||
const mockHandleSyncWorkflowDraft = jest.fn()
|
||||
const mockPublishWorkflow = jest.fn()
|
||||
const mockUpdatePublishedWorkflow = jest.fn()
|
||||
const mockResetWorkflowVersionHistory = jest.fn()
|
||||
const mockInvalidateAppTriggers = jest.fn()
|
||||
const mockFetchAppDetail = jest.fn()
|
||||
const mockSetAppDetail = jest.fn()
|
||||
const mockSetPublishedAt = jest.fn()
|
||||
const mockSetLastPublishedHasUserInput = jest.fn()
|
||||
|
||||
const mockWorkflowStoreSetState = jest.fn()
|
||||
const mockWorkflowStoreSetShowFeaturesPanel = jest.fn()
|
||||
|
||||
let workflowStoreState = {
|
||||
showFeaturesPanel: false,
|
||||
isRestoring: false,
|
||||
setShowFeaturesPanel: mockWorkflowStoreSetShowFeaturesPanel,
|
||||
setPublishedAt: mockSetPublishedAt,
|
||||
setLastPublishedHasUserInput: mockSetLastPublishedHasUserInput,
|
||||
}
|
||||
|
||||
const mockWorkflowStore = {
|
||||
getState: () => workflowStoreState,
|
||||
setState: mockWorkflowStoreSetState,
|
||||
}
|
||||
|
||||
let capturedAppPublisherProps: Record<string, unknown> | null = null
|
||||
|
||||
jest.mock('@/app/components/workflow/hooks', () => ({
|
||||
__esModule: true,
|
||||
useChecklist: (...args: unknown[]) => mockUseChecklist(...args),
|
||||
useChecklistBeforePublish: () => mockUseChecklistBeforePublish(),
|
||||
useNodesReadOnly: () => mockUseNodesReadOnly(),
|
||||
useNodesSyncDraft: () => mockUseNodesSyncDraft(),
|
||||
useIsChatMode: () => mockUseIsChatMode(),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/store', () => ({
|
||||
__esModule: true,
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const state: Record<string, unknown> = {
|
||||
publishedAt: null,
|
||||
draftUpdatedAt: null,
|
||||
toolPublished: false,
|
||||
lastPublishedHasUserInput: false,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
useWorkflowStore: () => mockWorkflowStore,
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/base/features/hooks', () => ({
|
||||
__esModule: true,
|
||||
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(),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({
|
||||
__esModule: true,
|
||||
default: () => mockUseNodes(),
|
||||
}))
|
||||
|
||||
jest.mock('reactflow', () => ({
|
||||
__esModule: true,
|
||||
useEdges: () => mockUseEdges(),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/app/app-publisher', () => ({
|
||||
__esModule: true,
|
||||
default: (props: Record<string, unknown>) => {
|
||||
capturedAppPublisherProps = props
|
||||
return (
|
||||
<div
|
||||
data-testid='app-publisher'
|
||||
data-disabled={String(Boolean(props.disabled))}
|
||||
data-publish-disabled={String(Boolean(props.publishDisabled))}
|
||||
/>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/service/use-workflow', () => ({
|
||||
__esModule: true,
|
||||
useInvalidateAppWorkflow: () => mockUpdatePublishedWorkflow,
|
||||
usePublishWorkflow: () => ({ mutateAsync: mockPublishWorkflow }),
|
||||
useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory,
|
||||
}))
|
||||
|
||||
jest.mock('@/service/use-tools', () => ({
|
||||
__esModule: true,
|
||||
useInvalidateAppTriggers: () => mockInvalidateAppTriggers,
|
||||
}))
|
||||
|
||||
jest.mock('@/service/apps', () => ({
|
||||
__esModule: true,
|
||||
fetchAppDetail: (...args: unknown[]) => mockFetchAppDetail(...args),
|
||||
}))
|
||||
|
||||
jest.mock('@/hooks/use-theme', () => ({
|
||||
__esModule: true,
|
||||
default: () => mockUseTheme(),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/app/store', () => ({
|
||||
__esModule: true,
|
||||
useStore: (selector: (state: { appDetail?: { id: string }; setAppDetail: typeof mockSetAppDetail }) => unknown) => mockUseAppStoreSelector(selector),
|
||||
}))
|
||||
|
||||
const createProviderContext = ({
|
||||
type = Plan.sandbox,
|
||||
isFetchedPlan = true,
|
||||
}: {
|
||||
type?: Plan
|
||||
isFetchedPlan?: boolean
|
||||
}) => ({
|
||||
plan: { type },
|
||||
isFetchedPlan,
|
||||
})
|
||||
|
||||
describe('FeaturesTrigger', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
capturedAppPublisherProps = null
|
||||
workflowStoreState = {
|
||||
showFeaturesPanel: false,
|
||||
isRestoring: false,
|
||||
setShowFeaturesPanel: mockWorkflowStoreSetShowFeaturesPanel,
|
||||
setPublishedAt: mockSetPublishedAt,
|
||||
setLastPublishedHasUserInput: mockSetLastPublishedHasUserInput,
|
||||
}
|
||||
|
||||
mockUseTheme.mockReturnValue({ theme: 'light' })
|
||||
mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false, getNodesReadOnly: () => false })
|
||||
mockUseChecklist.mockReturnValue([])
|
||||
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([])
|
||||
mockUseEdges.mockReturnValue([])
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({ appDetail: { id: 'app-id' }, setAppDetail: mockSetAppDetail }))
|
||||
mockFetchAppDetail.mockResolvedValue({ id: 'app-id' })
|
||||
mockPublishWorkflow.mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
|
||||
})
|
||||
|
||||
// Verifies the feature toggle button only appears in chatflow mode.
|
||||
describe('Rendering', () => {
|
||||
it('should not render the features button when not in chat mode', () => {
|
||||
// Arrange
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByRole('button', { name: /workflow\.common\.features/i })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the features button when in chat mode', () => {
|
||||
// Arrange
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /workflow\.common\.features/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply dark theme styling when theme is dark', () => {
|
||||
// Arrange
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
mockUseTheme.mockReturnValue({ theme: 'dark' })
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button', { name: /workflow\.common\.features/i })).toHaveClass('rounded-lg')
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies user clicks toggle the features panel visibility.
|
||||
describe('User Interactions', () => {
|
||||
it('should toggle features panel when clicked and nodes are editable', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: false, getNodesReadOnly: () => false })
|
||||
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /workflow\.common\.features/i }))
|
||||
|
||||
// Assert
|
||||
expect(mockWorkflowStoreSetShowFeaturesPanel).toHaveBeenCalledWith(true)
|
||||
})
|
||||
})
|
||||
|
||||
// Covers read-only gating that prevents toggling unless restoring.
|
||||
describe('Edge Cases', () => {
|
||||
it('should not toggle features panel when nodes are read-only and not restoring', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
mockUseIsChatMode.mockReturnValue(true)
|
||||
mockUseNodesReadOnly.mockReturnValue({ nodesReadOnly: true, getNodesReadOnly: () => true })
|
||||
workflowStoreState = {
|
||||
...workflowStoreState,
|
||||
isRestoring: false,
|
||||
}
|
||||
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /workflow\.common\.features/i }))
|
||||
|
||||
// Assert
|
||||
expect(mockWorkflowStoreSetShowFeaturesPanel).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies the publisher reflects the presence of workflow nodes.
|
||||
describe('Props', () => {
|
||||
it('should disable AppPublisher when there are no workflow nodes', () => {
|
||||
// Arrange
|
||||
mockUseIsChatMode.mockReturnValue(false)
|
||||
mockUseNodes.mockReturnValue([])
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(capturedAppPublisherProps?.disabled).toBe(true)
|
||||
expect(screen.getByTestId('app-publisher')).toHaveAttribute('data-disabled', 'true')
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies derived props passed into AppPublisher (variables, limits, and triggers).
|
||||
describe('Computed Props', () => {
|
||||
it('should append image input when file image upload is enabled', () => {
|
||||
// Arrange
|
||||
mockUseFeatures.mockImplementation((selector: (state: Record<string, unknown>) => unknown) => selector({
|
||||
features: { file: { image: { enabled: true } } },
|
||||
}))
|
||||
mockUseNodes.mockReturnValue([
|
||||
{ id: 'start', data: { type: BlockEnum.Start } },
|
||||
])
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
const inputs = (capturedAppPublisherProps?.inputs as unknown as Array<{ type?: string; variable?: string }>) || []
|
||||
expect(inputs).toContainEqual({
|
||||
type: InputVarType.files,
|
||||
variable: '__image',
|
||||
required: false,
|
||||
label: 'files',
|
||||
})
|
||||
})
|
||||
|
||||
it('should set startNodeLimitExceeded when sandbox entry limit is exceeded', () => {
|
||||
// Arrange
|
||||
mockUseNodes.mockReturnValue([
|
||||
{ id: 'start', data: { type: BlockEnum.Start } },
|
||||
{ id: 'trigger-1', data: { type: BlockEnum.TriggerWebhook } },
|
||||
{ id: 'trigger-2', data: { type: BlockEnum.TriggerSchedule } },
|
||||
{ id: 'end', data: { type: BlockEnum.End } },
|
||||
])
|
||||
|
||||
// Act
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
// Assert
|
||||
expect(capturedAppPublisherProps?.startNodeLimitExceeded).toBe(true)
|
||||
expect(capturedAppPublisherProps?.publishDisabled).toBe(true)
|
||||
expect(capturedAppPublisherProps?.hasTriggerNode).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies callbacks wired from AppPublisher to stores and draft syncing.
|
||||
describe('Callbacks', () => {
|
||||
it('should set toolPublished when AppPublisher refreshes data', () => {
|
||||
// Arrange
|
||||
render(<FeaturesTrigger />)
|
||||
const refresh = capturedAppPublisherProps?.onRefreshData as unknown as (() => void) | undefined
|
||||
expect(refresh).toBeDefined()
|
||||
|
||||
// Act
|
||||
refresh?.()
|
||||
|
||||
// Assert
|
||||
expect(mockWorkflowStoreSetState).toHaveBeenCalledWith({ toolPublished: true })
|
||||
})
|
||||
|
||||
it('should sync workflow draft when AppPublisher toggles on', () => {
|
||||
// Arrange
|
||||
render(<FeaturesTrigger />)
|
||||
const onToggle = capturedAppPublisherProps?.onToggle as unknown as ((state: boolean) => void) | undefined
|
||||
expect(onToggle).toBeDefined()
|
||||
|
||||
// Act
|
||||
onToggle?.(true)
|
||||
|
||||
// Assert
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should not sync workflow draft when AppPublisher toggles off', () => {
|
||||
// Arrange
|
||||
render(<FeaturesTrigger />)
|
||||
const onToggle = capturedAppPublisherProps?.onToggle as unknown as ((state: boolean) => void) | undefined
|
||||
expect(onToggle).toBeDefined()
|
||||
|
||||
// Act
|
||||
onToggle?.(false)
|
||||
|
||||
// Assert
|
||||
expect(mockHandleSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies publishing behavior across warnings, validation, and success.
|
||||
describe('Publishing', () => {
|
||||
it('should notify error and reject publish when checklist has warning nodes', async () => {
|
||||
// Arrange
|
||||
mockUseChecklist.mockReturnValue([{ id: 'warning' }])
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise<void>) | undefined
|
||||
expect(onPublish).toBeDefined()
|
||||
|
||||
// Act
|
||||
await expect(onPublish?.()).rejects.toThrow('Checklist has unresolved items')
|
||||
|
||||
// Assert
|
||||
expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'workflow.panel.checklistTip' })
|
||||
})
|
||||
|
||||
it('should reject publish when checklist before publish fails', async () => {
|
||||
// Arrange
|
||||
mockHandleCheckBeforePublish.mockResolvedValue(false)
|
||||
render(<FeaturesTrigger />)
|
||||
|
||||
const onPublish = capturedAppPublisherProps?.onPublish as unknown as (() => Promise<void>) | undefined
|
||||
expect(onPublish).toBeDefined()
|
||||
|
||||
// Act & Assert
|
||||
await expect(onPublish?.()).rejects.toThrow('Checklist failed')
|
||||
})
|
||||
|
||||
it('should publish workflow and update related stores when validation passes', async () => {
|
||||
// Arrange
|
||||
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()
|
||||
|
||||
// Act
|
||||
await onPublish?.()
|
||||
|
||||
// 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(mockFetchAppDetail).toHaveBeenCalledWith({ url: '/apps', id: 'app-id' })
|
||||
expect(mockSetAppDetail).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
|
||||
// Act
|
||||
await onPublish?.({ title: 'Test title', releaseNotes: 'Test notes' })
|
||||
|
||||
// Assert
|
||||
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 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()
|
||||
|
||||
// Act
|
||||
await onPublish?.()
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
})
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,149 @@
|
||||
import { render } 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', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
create: () => ({
|
||||
extend: () => async () => ({
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
json: async () => ({}),
|
||||
blob: async () => new Blob(),
|
||||
clone: () => ({
|
||||
status: 200,
|
||||
json: async () => ({}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/app/store', () => ({
|
||||
__esModule: true,
|
||||
useStore: (selector: (state: { appDetail?: App; setCurrentLogItem: typeof mockSetCurrentLogItem; setShowMessageLogModal: typeof mockSetShowMessageLogModal }) => unknown) => mockUseAppStoreSelector(selector),
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/header', () => ({
|
||||
__esModule: true,
|
||||
default: (props: HeaderProps) => {
|
||||
capturedHeaderProps = props
|
||||
return <div data-testid='workflow-header' />
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('@/service/workflow', () => ({
|
||||
__esModule: true,
|
||||
fetchWorkflowRunHistory: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/use-workflow', () => ({
|
||||
__esModule: true,
|
||||
useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory,
|
||||
}))
|
||||
|
||||
describe('WorkflowHeader', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
capturedHeaderProps = null
|
||||
appDetail = { id: 'app-id', mode: AppModeEnum.COMPLETION } as unknown as App
|
||||
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
})
|
||||
|
||||
// Verifies the wrapper renders the workflow header shell.
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Act
|
||||
render(<WorkflowHeader />)
|
||||
|
||||
// Assert
|
||||
expect(capturedHeaderProps).not.toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies chat mode affects which primary action is shown in the header.
|
||||
describe('Props', () => {
|
||||
it('should configure preview mode when app is in advanced chat mode', () => {
|
||||
// Arrange
|
||||
appDetail = { id: 'app-id', mode: AppModeEnum.ADVANCED_CHAT } as unknown as App
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
|
||||
// Act
|
||||
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)
|
||||
})
|
||||
|
||||
it('should configure run mode when app is not in advanced chat mode', () => {
|
||||
// Arrange
|
||||
appDetail = { id: 'app-id', mode: AppModeEnum.COMPLETION } as unknown as App
|
||||
mockUseAppStoreSelector.mockImplementation(selector => selector({
|
||||
appDetail,
|
||||
setCurrentLogItem: mockSetCurrentLogItem,
|
||||
setShowMessageLogModal: mockSetShowMessageLogModal,
|
||||
}))
|
||||
|
||||
// Act
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
// Verifies callbacks clear log state as expected.
|
||||
describe('User Interactions', () => {
|
||||
it('should clear log and close message modal when clearing history modal state', () => {
|
||||
// Arrange
|
||||
render(<WorkflowHeader />)
|
||||
|
||||
const clear = capturedHeaderProps?.normal?.runAndHistoryProps?.viewHistoryProps?.onClearLogAndMessageModal
|
||||
expect(clear).toBeDefined()
|
||||
|
||||
// Act
|
||||
clear?.()
|
||||
|
||||
// Assert
|
||||
expect(mockSetCurrentLogItem).toHaveBeenCalledWith()
|
||||
expect(mockSetShowMessageLogModal).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
|
||||
// Ensures restoring callback is wired to reset version history.
|
||||
describe('Edge Cases', () => {
|
||||
it('should use resetWorkflowVersionHistory as restore settled handler', () => {
|
||||
// Act
|
||||
render(<WorkflowHeader />)
|
||||
|
||||
// Assert
|
||||
expect(capturedHeaderProps?.restoring?.onRestoreSettled).toBe(mockResetWorkflowVersionHistory)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user