test(workflow): reorganize specs into __tests__ and align with shared test infrastructure (#33625)

Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Coding On Star
2026-03-18 16:40:28 +08:00
committed by GitHub
parent 387e5a345f
commit db4deb1d6b
39 changed files with 3538 additions and 203 deletions

View File

@ -0,0 +1,68 @@
import { render, screen } from '@testing-library/react'
import Meta from '../meta'
const mockFormatTime = vi.fn((value: number) => `formatted:${value}`)
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: mockFormatTime,
}),
}))
describe('Meta', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renders loading placeholders while the run is in progress', () => {
const { container } = render(<Meta status="running" />)
expect(container.querySelectorAll('.bg-text-quaternary')).toHaveLength(6)
expect(screen.queryByText('SUCCESS')).not.toBeInTheDocument()
expect(screen.queryByText('runLog.meta.steps')).toBeInTheDocument()
})
it.each([
['succeeded', 'SUCCESS'],
['partial-succeeded', 'PARTIAL SUCCESS'],
['exception', 'EXCEPTION'],
['failed', 'FAIL'],
['stopped', 'STOP'],
['paused', 'PENDING'],
] as const)('renders the %s status label', (status, label) => {
render(<Meta status={status} />)
expect(screen.getByText(label)).toBeInTheDocument()
})
it('renders explicit metadata values and hides steps when requested', () => {
render(
<Meta
status="succeeded"
executor="Alice"
startTime={1700000000000}
time={1.2349}
tokens={42}
steps={3}
showSteps={false}
/>,
)
expect(screen.getByText('Alice')).toBeInTheDocument()
expect(screen.getByText('formatted:1700000000000')).toBeInTheDocument()
expect(screen.getByText('1.235s')).toBeInTheDocument()
expect(screen.getByText('42 Tokens')).toBeInTheDocument()
expect(screen.queryByText('Run Steps')).not.toBeInTheDocument()
expect(mockFormatTime).toHaveBeenCalledWith(1700000000000, expect.any(String))
})
it('falls back to default values when metadata is missing', () => {
render(<Meta status="failed" />)
expect(screen.getByText('N/A')).toBeInTheDocument()
expect(screen.getAllByText('-')).toHaveLength(2)
expect(screen.getByText('0 Tokens')).toBeInTheDocument()
expect(screen.getByText('runLog.meta.steps').parentElement).toHaveTextContent('1')
expect(mockFormatTime).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,137 @@
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { FileResponse } from '@/types/workflow'
import { render, screen } from '@testing-library/react'
import { TransferMethod } from '@/types/app'
import OutputPanel from '../output-panel'
type FileOutput = FileResponse & { dify_model_identity: '__dify__file__' }
vi.mock('@/app/components/base/chat/chat/loading-anim', () => ({
default: () => <div data-testid="loading-anim" />,
}))
vi.mock('@/app/components/base/file-uploader', () => ({
FileList: ({ files }: { files: FileEntity[] }) => (
<div data-testid="file-list">{files.map(file => file.name).join(', ')}</div>
),
}))
vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>,
}))
vi.mock('@/app/components/workflow/run/status-container', () => ({
default: ({ status, children }: { status: string, children?: React.ReactNode }) => (
<div data-status={status} data-testid="status-container">{children}</div>
),
}))
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({
language,
value,
height,
}: {
language: string
value: string
height?: number
}) => (
<div data-height={height} data-language={language} data-testid="code-editor" data-value={value}>
{value}
</div>
),
}))
const createFileOutput = (overrides: Partial<FileOutput> = {}): FileOutput => ({
dify_model_identity: '__dify__file__',
related_id: 'file-1',
extension: 'pdf',
filename: 'report.pdf',
size: 128,
mime_type: 'application/pdf',
transfer_method: TransferMethod.local_file,
type: 'document',
url: 'https://example.com/report.pdf',
upload_file_id: 'upload-1',
remote_url: '',
...overrides,
})
describe('OutputPanel', () => {
it('renders the loading animation while the workflow is running', () => {
render(<OutputPanel isRunning />)
expect(screen.getByTestId('loading-anim')).toBeInTheDocument()
})
it('renders the failed status container when there is an error', () => {
render(<OutputPanel error="Execution failed" />)
expect(screen.getByTestId('status-container')).toHaveAttribute('data-status', 'failed')
expect(screen.getByText('Execution failed')).toBeInTheDocument()
})
it('renders the no-output placeholder when there are no outputs', () => {
render(<OutputPanel />)
expect(screen.getByTestId('markdown')).toHaveTextContent('No Output')
})
it('renders a plain text output as markdown', () => {
render(<OutputPanel outputs={{ answer: 'Hello Dify' }} />)
expect(screen.getByTestId('markdown')).toHaveTextContent('Hello Dify')
})
it('renders array text outputs as joined markdown content', () => {
render(<OutputPanel outputs={{ answer: ['Line 1', 'Line 2'] }} />)
expect(screen.getByTestId('markdown')).toHaveTextContent(/Line 1\s+Line 2/)
})
it('renders a file list for a single file output', () => {
render(<OutputPanel outputs={{ attachment: createFileOutput() }} />)
expect(screen.getByTestId('file-list')).toHaveTextContent('report.pdf')
})
it('renders a file list for an array of file outputs', () => {
render(
<OutputPanel
outputs={{
attachments: [
createFileOutput(),
createFileOutput({
related_id: 'file-2',
filename: 'summary.md',
extension: 'md',
mime_type: 'text/markdown',
type: 'custom',
upload_file_id: 'upload-2',
url: 'https://example.com/summary.md',
}),
],
}}
/>,
)
expect(screen.getByTestId('file-list')).toHaveTextContent('report.pdf, summary.md')
})
it('renders structured outputs inside the code editor when height is available', () => {
render(<OutputPanel height={200} outputs={{ answer: 'hello', score: 1 }} />)
expect(screen.getByTestId('code-editor')).toHaveAttribute('data-language', 'json')
expect(screen.getByTestId('code-editor')).toHaveAttribute('data-height', '92')
expect(screen.getByTestId('code-editor')).toHaveAttribute('data-value', `{
"answer": "hello",
"score": 1
}`)
})
it('skips the code editor when structured outputs have no positive height', () => {
render(<OutputPanel height={0} outputs={{ answer: 'hello', score: 1 }} />)
expect(screen.queryByTestId('code-editor')).not.toBeInTheDocument()
})
})

View File

@ -0,0 +1,88 @@
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { TransferMethod } from '@/types/app'
import ResultText from '../result-text'
vi.mock('@/app/components/base/chat/chat/loading-anim', () => ({
default: () => <div data-testid="loading-anim" />,
}))
vi.mock('@/app/components/base/file-uploader', () => ({
FileList: ({ files }: { files: FileEntity[] }) => (
<div data-testid="file-list">{files.map(file => file.name).join(', ')}</div>
),
}))
vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>,
}))
vi.mock('@/app/components/workflow/run/status-container', () => ({
default: ({ status, children }: { status: string, children?: React.ReactNode }) => (
<div data-status={status} data-testid="status-container">{children}</div>
),
}))
describe('ResultText', () => {
it('renders the loading animation while waiting for a text result', () => {
render(<ResultText isRunning />)
expect(screen.getByTestId('loading-anim')).toBeInTheDocument()
})
it('renders the error state when the run fails', () => {
render(<ResultText error="Run failed" />)
expect(screen.getByTestId('status-container')).toHaveAttribute('data-status', 'failed')
expect(screen.getByText('Run failed')).toBeInTheDocument()
})
it('renders the empty-state call to action and forwards clicks', () => {
const onClick = vi.fn()
render(<ResultText onClick={onClick} />)
expect(screen.getByText('runLog.resultEmpty.title')).toBeInTheDocument()
fireEvent.click(screen.getByText('runLog.resultEmpty.link'))
expect(onClick).toHaveBeenCalledTimes(1)
})
it('does not render the empty state for paused runs', () => {
render(<ResultText isPaused />)
expect(screen.queryByText('runLog.resultEmpty.title')).not.toBeInTheDocument()
})
it('renders markdown content when text outputs are available', () => {
render(<ResultText outputs="hello workflow" />)
expect(screen.getByTestId('markdown')).toHaveTextContent('hello workflow')
})
it('renders file groups when file outputs are available', () => {
render(
<ResultText
allFiles={[
{
varName: 'attachments',
list: [
{
id: 'file-1',
name: 'report.pdf',
size: 128,
type: 'application/pdf',
progress: 100,
transferMethod: TransferMethod.local_file,
supportFileType: 'document',
} satisfies FileEntity,
],
},
]}
/>,
)
expect(screen.getByText('attachments')).toBeInTheDocument()
expect(screen.getByTestId('file-list')).toHaveTextContent('report.pdf')
})
})

View File

@ -0,0 +1,131 @@
import type { WorkflowPausedDetailsResponse } from '@/models/log'
import { render, screen } from '@testing-library/react'
import Status from '../status'
const mockDocLink = vi.fn((path: string) => `https://docs.example.com${path}`)
const mockUseWorkflowPausedDetails = vi.fn()
vi.mock('@/context/i18n', () => ({
useDocLink: () => mockDocLink,
}))
vi.mock('@/service/use-log', () => ({
useWorkflowPausedDetails: (params: { workflowRunId: string, enabled?: boolean }) => mockUseWorkflowPausedDetails(params),
}))
const createPausedDetails = (overrides: Partial<WorkflowPausedDetailsResponse> = {}): WorkflowPausedDetailsResponse => ({
paused_at: '2026-03-18T00:00:00Z',
paused_nodes: [],
...overrides,
})
describe('Status', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseWorkflowPausedDetails.mockReturnValue({ data: undefined })
})
it('renders the running status and loading placeholders', () => {
render(<Status status="running" workflowRunId="run-1" />)
expect(screen.getByText('Running')).toBeInTheDocument()
expect(document.querySelectorAll('.bg-text-quaternary')).toHaveLength(2)
expect(mockUseWorkflowPausedDetails).toHaveBeenCalledWith({
workflowRunId: 'run-1',
enabled: false,
})
})
it('renders the listening label when the run is waiting for input', () => {
render(<Status status="running" isListening workflowRunId="run-2" />)
expect(screen.getByText('Listening')).toBeInTheDocument()
})
it('renders succeeded metadata values', () => {
render(<Status status="succeeded" time={1.234} tokens={8} />)
expect(screen.getByText('SUCCESS')).toBeInTheDocument()
expect(screen.getByText('1.234s')).toBeInTheDocument()
expect(screen.getByText('8 Tokens')).toBeInTheDocument()
})
it('renders stopped fallbacks when time and tokens are missing', () => {
render(<Status status="stopped" />)
expect(screen.getByText('STOP')).toBeInTheDocument()
expect(screen.getByText('-')).toBeInTheDocument()
expect(screen.getByText('0 Tokens')).toBeInTheDocument()
})
it('renders failed details and the partial-success exception tip', () => {
render(<Status status="failed" error="Something broke" exceptionCounts={2} />)
expect(screen.getByText('FAIL')).toBeInTheDocument()
expect(screen.getByText('Something broke')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.common.errorHandle.partialSucceeded.tip:{"num":2}')).toBeInTheDocument()
})
it('renders the partial-succeeded warning summary', () => {
render(<Status status="partial-succeeded" exceptionCounts={3} />)
expect(screen.getByText('PARTIAL SUCCESS')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.common.errorHandle.partialSucceeded.tip:{"num":3}')).toBeInTheDocument()
})
it('renders the exception learn-more link', () => {
render(<Status status="exception" error="Bad request" />)
const learnMoreLink = screen.getByRole('link', { name: 'workflow.common.learnMore' })
expect(screen.getByText('EXCEPTION')).toBeInTheDocument()
expect(learnMoreLink).toHaveAttribute('href', 'https://docs.example.com/use-dify/debug/error-type')
expect(mockDocLink).toHaveBeenCalledWith('/use-dify/debug/error-type')
})
it('renders paused placeholders when pause details have not loaded yet', () => {
render(<Status status="paused" workflowRunId="run-3" />)
expect(screen.getByText('PENDING')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.humanInput.log.reason')).toBeInTheDocument()
expect(document.querySelectorAll('.bg-text-quaternary')).toHaveLength(3)
expect(mockUseWorkflowPausedDetails).toHaveBeenCalledWith({
workflowRunId: 'run-3',
enabled: true,
})
})
it('renders paused human-input reasons and backstage URLs', () => {
mockUseWorkflowPausedDetails.mockReturnValue({
data: createPausedDetails({
paused_nodes: [
{
node_id: 'node-1',
node_title: 'Need review',
pause_type: {
type: 'human_input',
form_id: 'form-1',
backstage_input_url: 'https://example.com/a',
},
},
{
node_id: 'node-2',
node_title: 'Need review 2',
pause_type: {
type: 'human_input',
form_id: 'form-2',
backstage_input_url: 'https://example.com/b',
},
},
],
}),
})
render(<Status status="paused" workflowRunId="run-4" />)
expect(screen.getByText('workflow.nodes.humanInput.log.reasonContent')).toBeInTheDocument()
expect(screen.getByText('workflow.nodes.humanInput.log.backstageInputURL')).toBeInTheDocument()
expect(screen.getByRole('link', { name: 'https://example.com/a' })).toHaveAttribute('href', 'https://example.com/a')
expect(screen.getByRole('link', { name: 'https://example.com/b' })).toHaveAttribute('href', 'https://example.com/b')
})
})