test(web): replace AP30/AP32 render doubles with real behavior (#45440)

This commit is contained in:
Stephen Zhou authored and GitHub committed 2026-11-03 09:22:25 +00:00
1 parent fbc33cae43
commit bb0052a8f7
7 files changed
+302 -742

No files matched your search

@@ -5,13 +5,8 @@ import { WorkflowRunningStatus } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import ResultTab from '../result-tab'
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({ value, readOnly }: { value: string; readOnly: boolean }) => (
<textarea aria-label="Raw output" value={value} readOnly={readOnly} />
),
}))
it('switches between readable workflow output with attachments and read-only raw output', async () => {
// TODO: ResultTab stringifies already serialized content again; parse once must recover the output object.
it.fails('switches between readable workflow output with attachments and read-only raw output', async () => {
const file: FileEntity = {
id: 'file-1',
name: 'report.txt',
@@ -36,7 +31,9 @@ it('switches between readable workflow output with attachments and read-only raw
expect(screen.getByText('report.txt')).toBeInTheDocument()
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
rerender(<ResultTab currentTab="DETAIL" content={content} data={data} />)
expect(screen.getByRole('textbox', { name: 'Raw output' })).toHaveValue(content)
expect(screen.getByRole('textbox', { name: 'Raw output' })).toHaveAttribute('readonly')
expect(screen.getByRole('textbox')).toHaveAttribute('readonly')
expect(screen.queryByText('attachments')).not.toBeInTheDocument()
expect(JSON.parse((screen.getByRole('textbox') as HTMLTextAreaElement).value)).toEqual({
answer: 'ok',
})
})
@@ -1,4 +1,3 @@
import type { BlockEnum } from '@/app/components/workflow/types'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
@@ -6,19 +5,6 @@ import { describe, expect, it, vi } from 'vite-plus/test'
import { useLocale } from '#i18n'
import ToolCallItem from '../tool-call'
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({ title, value }: { title: React.ReactNode; value: string | object }) => (
<section aria-label={typeof title === 'string' ? title : undefined} data-testid="code-editor">
<h3>{title}</h3>
<div>{JSON.stringify(value)}</div>
</section>
),
}))
vi.mock('@/app/components/workflow/block-icon', () => ({
default: ({ type }: { type: BlockEnum }) => <div data-testid="block-icon" data-type={type} />,
}))
vi.mock('#i18n', async (importOriginal) => ({
...(await importOriginal<typeof import('#i18n')>()),
useLocale: vi.fn(() => 'en'),
@@ -64,16 +50,17 @@ describe('ToolCallItem', () => {
const user = userEvent.setup()
render(<ToolCallItem toolCall={mockToolCall} isLLM={false} />)
expect(screen.queryByTestId('code-editor')).not.toBeInTheDocument()
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
await user.click(screen.getByText('Test Tool Label'))
const editors = screen.getAllByTestId('code-editor')
expect(editors[0]).toHaveTextContent('INPUT')
expect(editors[0]).toHaveTextContent('{"query":"hello"}')
expect(editors[1]).toHaveTextContent('OUTPUT')
expect(editors[1]).toHaveTextContent('{"result":"world"}')
const editors = screen.getAllByRole('textbox')
expect(screen.getByText('INPUT')).toBeInTheDocument()
expect(JSON.parse((editors[0] as HTMLTextAreaElement).value)).toEqual({ query: 'hello' })
expect(screen.getByText('OUTPUT')).toBeInTheDocument()
expect(JSON.parse((editors[1] as HTMLTextAreaElement).value)).toEqual({ result: 'world' })
for (const editor of editors) expect(editor).toHaveAttribute('readonly')
await user.click(screen.getByText('Test Tool Label'))
expect(screen.queryByTestId('code-editor')).not.toBeInTheDocument()
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})
it('should display error message when status is error', async () => {
@@ -112,15 +99,13 @@ describe('ToolCallItem', () => {
/>,
)
await user.click(screen.getByText('LLM'))
expect(screen.getByRole('heading', { name: 'OBSERVATION' })).toBeInTheDocument()
expect(screen.getByText('"Tool returned a result"')).toBeInTheDocument()
expect(
screen.getByRole('heading', { name: isFinal ? 'FINAL ANSWER' : 'THOUGHT' }),
).toBeInTheDocument()
expect(
screen.queryByRole('heading', { name: isFinal ? 'THOUGHT' : 'FINAL ANSWER' }),
).not.toBeInTheDocument()
expect(screen.getByText('"Use the result"')).toBeInTheDocument()
expect(screen.getByText('OBSERVATION')).toBeInTheDocument()
expect(screen.getAllByRole('textbox')[2]).toHaveValue(
JSON.stringify('Tool returned a result'),
)
expect(screen.getByText(isFinal ? 'FINAL ANSWER' : 'THOUGHT')).toBeInTheDocument()
expect(screen.queryByText(isFinal ? 'THOUGHT' : 'FINAL ANSWER')).not.toBeInTheDocument()
expect(screen.getAllByRole('textbox')[3]).toHaveValue(JSON.stringify('Use the result'))
},
)
})
@@ -1,18 +1,9 @@
import type { AgentIterationLogResponse } from '@dify/contracts/api/console/apps/types.gen'
import { render, screen, within } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vite-plus/test'
import { describe, expect, it } from 'vite-plus/test'
import TracingPanel from '../tracing'
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({ title, value }: { title: React.ReactNode; value: string | object }) => (
<div data-testid="code-editor">
{title}
{typeof value === 'string' ? value : JSON.stringify(value)}
</div>
),
}))
const createIteration = (thought: string, tokens: number): AgentIterationLogResponse => ({
created_at: '',
files: [],
@@ -52,16 +43,19 @@ describe('TracingPanel', () => {
for (const llm of screen.getAllByText('LLM')) await user.click(llm)
const editors = screen
.getAllByTestId('code-editor')
.filter((editor) => /Thought \d/.test(editor.textContent ?? ''))
expect(editors.map((editor) => editor.textContent)).toEqual([
'THOUGHTThought 1',
'THOUGHTThought 2',
'FINAL ANSWERThought 3',
const editors = screen.getAllByRole('textbox') as HTMLTextAreaElement[]
expect(editors.map((editor) => JSON.parse(editor.value))).toEqual([
'',
'Thought 1',
'',
'Thought 2',
'',
'Thought 3',
])
expect(within(editors[2]!).getByText('FINAL ANSWER')).toBeInTheDocument()
expect(screen.getAllByText('THOUGHT')).toHaveLength(2)
for (const editor of editors) expect(editor).toHaveAttribute('readonly')
expect(
screen.getAllByText(/^(?:THOUGHT|FINAL ANSWER)$/).map((title) => title.textContent),
).toEqual(['THOUGHT', 'THOUGHT', 'FINAL ANSWER'])
expect(screen.getByText('FINAL ANSWER')).toBeInTheDocument()
})
})
@@ -1,18 +1,14 @@
import type { HumanInputFilledFormData } from '@/types/workflow'
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vite-plus/test'
import { describe, expect, it } from 'vite-plus/test'
import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { SubmittedHumanInputContent } from '../submitted'
vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content }: { content: string }) => <div>{content}</div>,
}))
describe('SubmittedHumanInputContent Integration', () => {
const mockFormData: HumanInputFilledFormData = {
form_id: 'form-1',
rendered_content: 'Rendered **Markdown** content',
rendered_content: 'Rendered [Markdown](https://example.com/submitted) content',
action_id: 'btn_1',
action_text: 'Submit Action',
node_id: 'node_1',
@@ -40,7 +36,7 @@ describe('SubmittedHumanInputContent Integration', () => {
)
expect(screen.getByText('approved')).toBeInTheDocument()
expect(screen.queryByText('Rendered **Markdown** content')).not.toBeInTheDocument()
expect(screen.queryByText('Markdown')).not.toBeInTheDocument()
})
it('should render submitted select and file fields with the original form layout', () => {
@@ -89,7 +85,7 @@ describe('SubmittedHumanInputContent Integration', () => {
it.each([undefined, {}])(
'falls back to rendered markdown without submitted values: %j',
(submittedData) => {
async (submittedData) => {
render(
<SubmittedHumanInputContent
formData={{
@@ -99,7 +95,10 @@ describe('SubmittedHumanInputContent Integration', () => {
/>,
)
expect(screen.getByText('Rendered **Markdown** content')).toBeInTheDocument()
expect(await screen.findByRole('link', { name: 'Markdown' })).toHaveAttribute(
'href',
'https://example.com/submitted',
)
expect(
screen.getByText('workflowHumanInput.nodes.humanInput.userActions.triggered'),
).toBeInTheDocument()
@@ -119,6 +118,6 @@ describe('SubmittedHumanInputContent Integration', () => {
)
expect(screen.getByText('approved')).toBeInTheDocument()
expect(screen.queryByText('Rendered **Markdown** content')).not.toBeInTheDocument()
expect(screen.queryByText('Markdown')).not.toBeInTheDocument()
})
})
@@ -2,100 +2,17 @@ import type {
ChildChunkResponse,
SegmentResponse,
} from '@dify/contracts/api/console/datasets/types.gen'
import type { SegmentListContextValue } from '@/app/components/datasets/documents/detail/completed'
import type { DocumentContextValue } from '@/app/components/datasets/documents/detail/context'
import type { Attachment, ParentMode } from '@/models/datasets'
import { render, screen, waitFor } from '@testing-library/react'
import type { ComponentProps } from 'react'
import type { DocumentContextValue } from '../../../context'
import type { Attachment } from '@/models/datasets'
import { screen, waitFor, within } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { useState } from 'react'
import { ChunkingMode } from '@/models/datasets'
import { renderWithConsoleQuery } from '@/test/console/query-data'
import { DocumentContext } from '../../../context'
import SegmentCard from '../index'
// Context Mocks - need to control test scenarios
const mockDocForm: { current: ChunkingMode } = { current: ChunkingMode.text }
const mockParentMode = { current: 'paragraph' as ParentMode }
const mockCanEdit = { current: true }
vi.mock('../../../context', () => ({
useDocumentContext: (selector: (value: DocumentContextValue) => unknown) => {
const value: DocumentContextValue = {
datasetId: 'test-dataset-id',
documentId: 'test-document-id',
docForm: mockDocForm.current,
parentMode: mockParentMode.current,
canEdit: mockCanEdit.current,
}
return selector(value)
},
}))
const mockIsCollapsed = { current: true }
vi.mock('../../index', () => ({
useSegmentListContext: (selector: (value: SegmentListContextValue) => unknown) => {
const value: SegmentListContextValue = {
isCollapsed: mockIsCollapsed.current,
fullScreen: false,
toggleFullScreen: vi.fn(),
currSegment: { showModal: false },
currChildChunk: { showModal: false },
}
return selector(value)
},
}))
// Component Mocks - components with complex dependencies
// StatusItem uses React Query hooks which require QueryClientProvider
vi.mock('../../../../status-item', () => ({
default: ({
status,
reverse,
textCls,
}: {
status: string
reverse?: boolean
textCls?: string
}) => (
<div data-testid="status-item" data-status={status} data-reverse={reverse} className={textCls}>
Status: {status}
</div>
),
}))
// ImageList has deep dependency: FileThumb → file-uploader → react-pdf-highlighter (ESM)
vi.mock('@/app/components/datasets/common/image-list', () => ({
default: ({
images,
size,
className,
}: {
images: Array<{ sourceUrl: string; name: string }>
size?: string
className?: string
}) => (
<div
data-testid="image-list"
data-image-count={images.length}
data-size={size}
className={className}
>
{images.map((img, idx: number) => (
<img key={idx} src={img.sourceUrl} alt={img.name} />
))}
</div>
),
}))
// Markdown loads its code blocks through next/dynamic
vi.mock('@/app/components/base/markdown', () => ({
Markdown: ({ content, className }: { content: string; className?: string }) => (
<div data-testid="markdown" className={`markdown-body ${className || ''}`}>
{content}
</div>
),
}))
const createMockAttachment = (overrides: Partial<Attachment> = {}): Attachment => ({
id: 'attachment-1',
name: 'test-image.png',
@@ -154,593 +71,276 @@ const createMockSegmentDetail = (
const defaultFocused = { segmentIndex: false, segmentContent: false }
// Tests
type CardProps = ComponentProps<typeof SegmentCard>
function renderCard(props: Partial<CardProps> = {}, context: DocumentContextValue = {}) {
const document = {
docForm: ChunkingMode.text,
parentMode: 'paragraph' as const,
canEdit: true,
...context,
}
return renderWithConsoleQuery(
<DocumentContext.Provider value={document}>
<SegmentCard
loading={false}
detail={createMockSegmentDetail()}
focused={defaultFocused}
{...props}
/>
</DocumentContext.Provider>,
)
}
describe('SegmentCard', () => {
beforeEach(() => {
vi.clearAllMocks()
mockDocForm.current = ChunkingMode.text
mockParentMode.current = 'paragraph'
mockIsCollapsed.current = true
mockCanEdit.current = true
})
it.each([true, false])(
'should disable single chunk mutations without edit permission (enabled: %s)',
'prevents chunk mutations without edit permission (enabled: %s)',
async (enabled) => {
mockCanEdit.current = false
const user = userEvent.setup()
const onClickEdit = vi.fn()
const onChangeSwitch = vi.fn()
const onDelete = vi.fn()
render(
<SegmentCard
loading={false}
detail={createMockSegmentDetail({ enabled })}
embeddingAvailable
focused={defaultFocused}
onClickEdit={onClickEdit}
onChangeSwitch={onChangeSwitch}
onDelete={onDelete}
/>,
const onChangeSwitch = vi.fn()
renderCard(
{
detail: createMockSegmentDetail({ enabled }),
embeddingAvailable: true,
onClickEdit,
onDelete,
onChangeSwitch,
},
{ canEdit: false },
)
const editButton = screen.getByRole('button', { name: 'common.operation.edit' })
const deleteButton = screen.getByRole('button', { name: 'common.operation.delete' })
const statusSwitch = screen.getByRole('switch')
expect(editButton).toBeDisabled()
expect(deleteButton).toBeDisabled()
expect(statusSwitch).toHaveAttribute('aria-disabled', 'true')
await user.click(editButton)
await user.click(deleteButton)
await user.click(statusSwitch)
for (const name of ['common.operation.edit', 'common.operation.delete']) {
const button = screen.getByRole('button', { name })
expect(button).toBeDisabled()
await user.click(button)
}
const toggle = screen.getByRole('switch')
expect(toggle).toHaveAttribute('aria-disabled', 'true')
await user.click(toggle)
expect(onClickEdit).not.toHaveBeenCalled()
expect(onChangeSwitch).not.toHaveBeenCalled()
expect(onDelete).not.toHaveBeenCalled()
expect(onChangeSwitch).not.toHaveBeenCalled()
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument()
expect(
screen.getByText(`datasetDocuments.list.status.${enabled ? 'enabled' : 'disabled'}`),
).toBeInTheDocument()
},
)
describe('Rendering', () => {
it('should render segment index tag with correct position', () => {
const detail = createMockSegmentDetail({ position: 5 })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText(/Chunk-05/i)).toBeInTheDocument()
})
it('should render word count text', () => {
const detail = createMockSegmentDetail({ word_count: 250 })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(
screen.getByText('250 datasetDocuments.segment.characters:{"count":250}'),
).toBeInTheDocument()
})
it('should render hit count text', () => {
const detail = createMockSegmentDetail({ hit_count: 42 })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText('42 datasetDocuments.segment.hitCount')).toBeInTheDocument()
})
})
// Props Tests
describe('Props', () => {
it('should handle archived prop correctly - switch should be disabled', () => {
const detail = createMockSegmentDetail()
render(
<SegmentCard
loading={false}
detail={detail}
archived={true}
embeddingAvailable={true}
focused={defaultFocused}
/>,
)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('aria-disabled', 'true')
})
it('should not show action buttons when embeddingAvailable is false', () => {
const detail = createMockSegmentDetail()
render(
<SegmentCard
loading={false}
detail={detail}
embeddingAvailable={false}
focused={defaultFocused}
/>,
)
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
})
})
describe('State Management', () => {
it('should close delete confirmation modal when cancel is clicked', async () => {
const user = userEvent.setup()
const detail = createMockSegmentDetail()
const onDelete = vi.fn()
render(
<SegmentCard
loading={false}
detail={detail}
onDelete={onDelete}
embeddingAvailable={true}
focused={defaultFocused}
/>,
)
const deleteButton = screen.getByRole('button', { name: 'common.operation.delete' })
await user.click(deleteButton)
await waitFor(() => {
expect(screen.getByText('datasetDocuments.segment.delete')).toBeInTheDocument()
})
await user.click(screen.getByText('common.operation.cancel'))
await waitFor(() => {
expect(screen.queryByText('datasetDocuments.segment.delete')).not.toBeInTheDocument()
})
expect(onDelete).not.toHaveBeenCalled()
})
})
describe('Callbacks', () => {
it('opens a chunk from its keyboard-accessible detail button', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
render(
<SegmentCard
loading={false}
detail={createMockSegmentDetail()}
onClick={onClick}
focused={defaultFocused}
/>,
)
const detailButton = screen.getByRole('button', {
name: 'datasetDocuments.segment.chunk-01 datasetDocuments.segment.chunkDetail',
})
detailButton.focus()
await user.keyboard('{Enter}')
expect(onClick).toHaveBeenCalledTimes(1)
})
it('should call onClick when card is clicked in general mode', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
const detail = createMockSegmentDetail()
mockDocForm.current = ChunkingMode.text
render(
<SegmentCard loading={false} detail={detail} onClick={onClick} focused={defaultFocused} />,
)
const card = screen.getByTestId('segment-card')
await user.click(card)
expect(onClick).toHaveBeenCalledTimes(1)
})
it('should not call onClick when card is clicked in full-doc mode', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
const detail = createMockSegmentDetail()
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'full-doc'
render(
<SegmentCard loading={false} detail={detail} onClick={onClick} focused={defaultFocused} />,
)
const card = screen.getByTestId('segment-card')
await user.click(card)
expect(onClick).not.toHaveBeenCalled()
})
it('should call onClick when view more button is clicked in full-doc mode', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
const detail = createMockSegmentDetail()
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'full-doc'
render(
<SegmentCard loading={false} detail={detail} onClick={onClick} focused={defaultFocused} />,
)
const viewMoreButton = screen.getByRole('button', { name: /viewMore/i })
await user.click(viewMoreButton)
expect(onClick).toHaveBeenCalledTimes(1)
})
it('should call onDelete when confirm delete is clicked', async () => {
const user = userEvent.setup()
const onDelete = vi.fn().mockResolvedValue(undefined)
const detail = createMockSegmentDetail({ id: 'test-segment-id' })
render(
<SegmentCard
loading={false}
detail={detail}
onDelete={onDelete}
embeddingAvailable={true}
focused={defaultFocused}
/>,
)
const deleteButton = screen.getByRole('button', { name: 'common.operation.delete' })
await user.click(deleteButton)
await waitFor(() => {
expect(screen.getByText('datasetDocuments.segment.delete')).toBeInTheDocument()
})
await user.click(screen.getByText('common.operation.sure'))
await waitFor(() => {
expect(onDelete).toHaveBeenCalledWith('test-segment-id')
})
})
it('should call onChangeSwitch when switch is toggled', async () => {
const user = userEvent.setup()
const onChangeSwitch = vi.fn().mockResolvedValue(undefined)
const onClick = vi.fn()
const detail = createMockSegmentDetail({
id: 'test-segment-id',
enabled: true,
status: 'completed',
})
render(
<SegmentCard
loading={false}
detail={detail}
onChangeSwitch={onChangeSwitch}
onClick={onClick}
embeddingAvailable={true}
focused={defaultFocused}
/>,
)
const switchElement = screen.getByRole('switch')
expect(switchElement).toBeChecked()
await user.click(switchElement)
expect(onClick).not.toHaveBeenCalled()
await waitFor(() => {
expect(onChangeSwitch).toHaveBeenCalledWith(false, 'test-segment-id')
})
})
it('should stop propagation when edit button is clicked', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
const onClickEdit = vi.fn()
const detail = createMockSegmentDetail()
render(
<SegmentCard
loading={false}
detail={detail}
onClick={onClick}
onClickEdit={onClickEdit}
embeddingAvailable={true}
focused={defaultFocused}
/>,
)
const editButton = screen.getByRole('button', { name: 'common.operation.edit' })
await user.click(editButton)
expect(onClickEdit).toHaveBeenCalledTimes(1)
expect(onClick).not.toHaveBeenCalled()
})
})
// Memoization Logic Tests
describe('Chunk labels', () => {
it('labels parent chunks', () => {
mockDocForm.current = ChunkingMode.parentChild
const detail = createMockSegmentDetail()
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText(/datasetDocuments\.segment\.parentChunk/i)).toBeInTheDocument()
})
it('marks a chunk edited after its creation time', () => {
mockDocForm.current = ChunkingMode.text
const detail = createMockSegmentDetail({
created_at: 1700000000,
it('renders chunk identity, counts, edited marker and keywords', () => {
renderCard({
detail: createMockSegmentDetail({
position: 5,
word_count: 250,
hit_count: 42,
updated_at: 1700000001,
})
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText('datasetDocuments.segment.edited')).toBeInTheDocument()
})
it('should not show edited badge when timestamps are equal', () => {
mockDocForm.current = ChunkingMode.text
const detail = createMockSegmentDetail({
created_at: 1700000000,
updated_at: 1700000000,
})
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.queryByText('datasetDocuments.segment.edited')).not.toBeInTheDocument()
})
it('should not show edited badge in full-doc mode', () => {
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'full-doc'
const detail = createMockSegmentDetail({
created_at: 1700000000,
updated_at: 1700000001,
})
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.queryByText('datasetDocuments.segment.edited')).not.toBeInTheDocument()
}),
})
expect(
screen.getByRole('button', {
name: 'datasetDocuments.segment.chunk-05 datasetDocuments.segment.chunkDetail',
}),
).toBeInTheDocument()
expect(
screen.getByText('250 datasetDocuments.segment.characters:{"count":250}'),
).toBeInTheDocument()
expect(screen.getByText('42 datasetDocuments.segment.hitCount')).toBeInTheDocument()
expect(screen.getByText('datasetDocuments.segment.edited')).toBeInTheDocument()
expect(screen.getByText('keyword1')).toBeInTheDocument()
expect(screen.getByText('keyword2')).toBeInTheDocument()
})
// Mode-specific Rendering Tests
describe('Mode-specific Rendering', () => {
it('should not render status item in full-doc mode', () => {
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'full-doc'
const detail = createMockSegmentDetail()
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
// In full-doc mode, status item should not render
expect(screen.queryByTestId('status-item')).not.toBeInTheDocument()
})
it.each([
{ archived: true, status: 'completed' as const },
{ archived: false, status: 'indexing' as const },
])('disables changes for $status / archived=$archived', ({ archived, status }) => {
renderCard({ archived, embeddingAvailable: true, detail: createMockSegmentDetail({ status }) })
expect(screen.getByRole('switch')).toHaveAttribute('aria-disabled', 'true')
})
// Child Segment List Tests
describe('Child Segment List', () => {
it('should render ChildSegmentList when in paragraph mode with child chunks', () => {
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'paragraph'
const childChunks = [
createMockChildChunk(),
createMockChildChunk({ id: 'child-2', position: 2 }),
]
const detail = createMockSegmentDetail({ child_chunks: childChunks })
it('hides mutation controls when embeddings are unavailable', () => {
renderCard({ embeddingAvailable: false })
expect(screen.queryByRole('switch')).not.toBeInTheDocument()
expect(
screen.queryByRole('button', { name: 'common.operation.delete' }),
).not.toBeInTheDocument()
})
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText(/2 datasetDocuments\.segment\.childChunks/)).toBeInTheDocument()
it('cancels deletion without submitting, then confirms the exact chunk', async () => {
const user = userEvent.setup()
const onDelete = vi.fn().mockResolvedValue(undefined)
renderCard({
onDelete,
embeddingAvailable: true,
detail: createMockSegmentDetail({ id: 'delete-me' }),
})
const remove = screen.getByRole('button', { name: 'common.operation.delete' })
await user.click(remove)
let dialog = await screen.findByRole('alertdialog', { name: 'datasetDocuments.segment.delete' })
await user.click(within(dialog).getByRole('button', { name: 'common.operation.cancel' }))
await waitFor(() => expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument())
expect(onDelete).not.toHaveBeenCalled()
await user.click(remove)
dialog = await screen.findByRole('alertdialog')
await user.click(within(dialog).getByRole('button', { name: 'common.operation.sure' }))
expect(onDelete).toHaveBeenCalledExactlyOnceWith('delete-me')
})
it('should not render ChildSegmentList when child_chunks is empty', () => {
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'paragraph'
const detail = createMockSegmentDetail({ child_chunks: [] })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.queryByText(/child chunk/i)).not.toBeInTheDocument()
})
it('should not render ChildSegmentList in full-doc mode', () => {
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'full-doc'
const childChunks = [createMockChildChunk()]
const detail = createMockSegmentDetail({ child_chunks: childChunks })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
// In full-doc mode, ChildSegmentList should not render
expect(screen.queryByText(/datasetDocuments\.segment\.childChunks/)).not.toBeInTheDocument()
})
it('should call handleAddNewChildChunk when add button is clicked', async () => {
const user = userEvent.setup()
mockDocForm.current = ChunkingMode.parentChild
mockParentMode.current = 'paragraph'
const handleAddNewChildChunk = vi.fn()
const childChunks = [createMockChildChunk()]
const detail = createMockSegmentDetail({ id: 'parent-id', child_chunks: childChunks })
render(
<SegmentCard
loading={false}
detail={detail}
handleAddNewChildChunk={handleAddNewChildChunk}
focused={defaultFocused}
/>,
it('opens details with the keyboard and keeps edit and switch actions separate', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
const onClickEdit = vi.fn()
const onChangeSwitch = vi.fn()
function Caller() {
const [detail, setDetail] = useState(createMockSegmentDetail())
return (
<DocumentContext.Provider value={{ docForm: ChunkingMode.text, canEdit: true }}>
<SegmentCard
loading={false}
detail={detail}
focused={defaultFocused}
embeddingAvailable
onClick={onClick}
onClickEdit={onClickEdit}
onChangeSwitch={async (enabled, id) => {
onChangeSwitch(enabled, id)
setDetail({ ...detail, enabled })
}}
/>
</DocumentContext.Provider>
)
const addButton = screen.getByText('common.operation.add')
await user.click(addButton)
expect(handleAddNewChildChunk).toHaveBeenCalledWith('parent-id')
})
}
renderWithConsoleQuery(<Caller />)
screen.getByRole('button', { name: /segment.chunkDetail/ }).focus()
await user.keyboard('{Enter}')
expect(onClick).toHaveBeenCalledOnce()
await user.click(screen.getByRole('button', { name: 'common.operation.edit' }))
expect(onClickEdit).toHaveBeenCalledOnce()
await user.click(screen.getByRole('switch'))
expect(onChangeSwitch).toHaveBeenCalledExactlyOnceWith(false, 'segment-1')
expect(screen.getByRole('switch')).not.toBeChecked()
expect(screen.getByText('datasetDocuments.list.status.disabled')).toBeInTheDocument()
expect(onClick).toHaveBeenCalledOnce()
})
// Keywords Display Tests
describe('Keywords Display', () => {
it('should render keywords with # prefix in general mode', () => {
mockDocForm.current = ChunkingMode.text
const detail = createMockSegmentDetail({ keywords: ['keyword1', 'keyword2'] })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText('keyword1')).toBeInTheDocument()
expect(screen.getByText('keyword2')).toBeInTheDocument()
})
it('should not render keywords in QA mode', () => {
mockDocForm.current = ChunkingMode.qa
const detail = createMockSegmentDetail({ keywords: ['keyword1'] })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
})
it('should not render keywords in parent-child mode', () => {
mockDocForm.current = ChunkingMode.parentChild
const detail = createMockSegmentDetail({ keywords: ['keyword1'] })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
})
it('shows a full-document preview and opens only through View more', async () => {
const user = userEvent.setup()
const onClick = vi.fn()
renderCard(
{
onClick,
detail: createMockSegmentDetail({
updated_at: 1700000001,
child_chunks: [createMockChildChunk()],
sign_content: '[Document preview](https://example.com/preview)',
}),
},
{ docForm: ChunkingMode.parentChild, parentMode: 'full-doc' },
)
const preview = await screen.findByRole('link', { name: 'Document preview' })
expect(preview).toHaveAttribute('href', 'https://example.com/preview')
await user.click(preview)
expect(onClick).not.toHaveBeenCalled()
expect(screen.queryByText('datasetDocuments.segment.edited')).not.toBeInTheDocument()
expect(screen.queryByText('datasetDocuments.list.status.enabled')).not.toBeInTheDocument()
expect(screen.queryByText(/segment.childChunks/)).not.toBeInTheDocument()
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'common.operation.viewMore' }))
expect(onClick).toHaveBeenCalledOnce()
})
// Images Display Tests
describe('Images Display', () => {
it('should render ImageList when attachments exist', () => {
const attachments = [createMockAttachment()]
const detail = createMockSegmentDetail({ attachments })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
// ImageList uses FileThumb which renders images
expect(screen.getByAltText('test-image.png')).toBeInTheDocument()
})
it('should not render ImageList when attachments is empty', () => {
const detail = createMockSegmentDetail({ attachments: [] })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.queryByAltText('test-image.png')).not.toBeInTheDocument()
})
it('renders parent child count and adds to the selected parent', async () => {
const user = userEvent.setup()
const handleAddNewChildChunk = vi.fn()
renderCard(
{
handleAddNewChildChunk,
detail: createMockSegmentDetail({
id: 'parent-id',
child_chunks: [
createMockChildChunk(),
createMockChildChunk({ id: 'child-2', position: 2 }),
],
}),
},
{ docForm: ChunkingMode.parentChild },
)
expect(screen.getByText(/2 datasetDocuments.segment.childChunks/)).toBeInTheDocument()
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'common.operation.add' }))
expect(handleAddNewChildChunk).toHaveBeenCalledExactlyOnceWith('parent-id')
})
// Edge Cases and Error Handling Tests
describe('Edge Cases and Error Handling', () => {
it('should handle switch being disabled when status is not completed', () => {
const detail = createMockSegmentDetail({ status: 'indexing' })
it('omits the child list for a parent with no children', () => {
renderCard({}, { docForm: ChunkingMode.parentChild })
expect(screen.queryByText(/segment.childChunks/)).not.toBeInTheDocument()
})
render(
<SegmentCard
loading={false}
detail={detail}
embeddingAvailable={true}
focused={defaultFocused}
/>,
)
const switchElement = screen.getByRole('switch')
expect(switchElement).toHaveAttribute('aria-disabled', 'true')
it('renders attachment thumbnails from the supplied source and omits them when empty', () => {
const { unmount } = renderCard({
detail: createMockSegmentDetail({ attachments: [createMockAttachment()] }),
})
const attachment = screen.getByRole('button', { name: 'test-image.png' })
expect(within(attachment).getByRole('img', { name: 'test-image.png' })).toHaveAttribute(
'src',
'https://example.com/test-image.png',
)
unmount()
renderCard()
expect(screen.queryByRole('button', { name: 'test-image.png' })).not.toBeInTheDocument()
})
it('should handle zero word count', () => {
const detail = createMockSegmentDetail({ word_count: 0 })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
it('renders question and answer Markdown instead of signed content in QA mode', async () => {
renderCard(
{
detail: createMockSegmentDetail({
content: '[Question](https://example.com/question)',
answer: '[Answer](https://example.com/answer)',
sign_content: 'Ignored signed content',
}),
},
{ docForm: ChunkingMode.qa },
)
expect(await screen.findByRole('link', { name: 'Question' })).toHaveAttribute(
'href',
'https://example.com/question',
)
expect(screen.getByRole('link', { name: 'Answer' })).toHaveAttribute(
'href',
'https://example.com/answer',
)
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
expect(screen.queryByText('Ignored signed content')).not.toBeInTheDocument()
expect(screen.queryByText('keyword1')).not.toBeInTheDocument()
})
it.each(['[Signed content](https://example.com/signed)', ''])(
'uses signed content when present, otherwise original content (%s)',
async (sign_content) => {
renderCard({
detail: createMockSegmentDetail({
content: '[Original content](https://example.com/original)',
sign_content,
answer: '',
}),
})
expect(
screen.getByText('0 datasetDocuments.segment.characters:{"count":0}'),
).toBeInTheDocument()
})
it('should handle zero hit count', () => {
const detail = createMockSegmentDetail({ hit_count: 0 })
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText('0 datasetDocuments.segment.hitCount')).toBeInTheDocument()
})
})
// Component Integration Tests
// All Props Variations Tests
describe('All Props Variations', () => {
it('should handle loading transition correctly', () => {
const detail = createMockSegmentDetail()
const { rerender } = render(
<SegmentCard loading={true} detail={detail} focused={defaultFocused} />,
await screen.findByRole('link', {
name: sign_content ? 'Signed content' : 'Original content',
}),
).toHaveAttribute(
'href',
sign_content ? 'https://example.com/signed' : 'https://example.com/original',
)
// When loading, content should not be visible
expect(screen.queryByText('Test signed content')).not.toBeInTheDocument()
rerender(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
// ChunkContent shows sign_content first
expect(screen.getByText('Test signed content')).toBeInTheDocument()
})
})
// ChunkContent QA Mode Tests - cover lines 25-49
describe('ChunkContent QA Mode', () => {
it('should render Q and A sections when answer is provided', () => {
const detail = createMockSegmentDetail({
content: 'This is the question content',
answer: 'This is the answer content',
sign_content: '',
})
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
// Should render Q label
expect(screen.getByText('Q')).toBeInTheDocument()
// Should render A label
expect(screen.getByText('A')).toBeInTheDocument()
// Should render question content
expect(screen.getByText('This is the question content')).toBeInTheDocument()
// Should render answer content
expect(screen.getByText('This is the answer content')).toBeInTheDocument()
})
it('should not render QA mode when answer is empty string', () => {
const detail = createMockSegmentDetail({
content: 'Regular content',
answer: '',
sign_content: 'Signed content',
})
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
// Should not render Q and A labels
expect(screen.queryByText('Q')).not.toBeInTheDocument()
expect(screen.queryByText('A')).not.toBeInTheDocument()
// Should render signed content instead
expect(screen.getByText('Signed content')).toBeInTheDocument()
})
})
},
)
// ChunkContent Non-QA Mode Tests - ensure full coverage
describe('ChunkContent Non-QA Mode', () => {
it('should fall back to content when sign_content is empty', () => {
const detail = createMockSegmentDetail({
content: 'Fallback content',
sign_content: '',
})
render(<SegmentCard loading={false} detail={detail} focused={defaultFocused} />)
expect(screen.getByText('Fallback content')).toBeInTheDocument()
})
it('withholds stale content while loading', () => {
renderCard({ loading: true })
expect(screen.queryByText('Test signed content')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: /segment.chunkDetail/ })).not.toBeInTheDocument()
})
})
@@ -1,15 +1,9 @@
import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vite-plus/test'
import { describe, expect, it } from 'vite-plus/test'
import LogViewer from '../log-viewer'
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({ value }: { value: unknown }) => (
<div data-testid="code-editor">{JSON.stringify(value)}</div>
),
}))
const createLog = (overrides: Partial<TriggerLogEntity> = {}): TriggerLogEntity => ({
id: 'log-1',
endpoint: 'https://example.com',
@@ -61,14 +55,18 @@ describe('LogViewer', () => {
screen.getByRole('button', { name: /pluginTrigger\.modal\.manual\.logs\.request/ }),
)
const editors = screen.getAllByTestId('code-editor')
const editors = screen.getAllByRole('textbox')
expect(editors.length).toBe(2)
expect(editors[0]).toHaveTextContent('"foo":"bar"')
expect(editors[1]).toHaveTextContent('"status_code":200')
expect(JSON.parse((editors[0] as HTMLTextAreaElement).value)).toEqual({
headers: createLog().request.headers,
data: { foo: 'bar' },
})
expect(JSON.parse((editors[1] as HTMLTextAreaElement).value)).toEqual(createLog().response)
for (const editor of editors) expect(editor).toHaveAttribute('readonly')
await user.click(
screen.getByRole('button', { name: /pluginTrigger\.modal\.manual\.logs\.request/ }),
)
expect(screen.queryByTestId('code-editor')).not.toBeInTheDocument()
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
})
it('should parse request data when it is raw JSON', async () => {
@@ -81,7 +79,9 @@ describe('LogViewer', () => {
screen.getByRole('button', { name: /pluginTrigger\.modal\.manual\.logs\.request/ }),
)
expect(screen.getAllByTestId('code-editor')[0]).toHaveTextContent('"hello":1')
expect(
JSON.parse((screen.getAllByRole('textbox')[0] as HTMLTextAreaElement).value).data,
).toEqual({ hello: 1 })
})
it('should fallback to raw payload when decoding fails', async () => {
@@ -94,7 +94,9 @@ describe('LogViewer', () => {
screen.getByRole('button', { name: /pluginTrigger\.modal\.manual\.logs\.request/ }),
)
expect(screen.getAllByTestId('code-editor')[0]).toHaveTextContent('payload=%E0%A4%A')
expect(JSON.parse((screen.getAllByRole('textbox')[0] as HTMLTextAreaElement).value).data).toBe(
'payload=%E0%A4%A',
)
})
it('should keep request data string when JSON parsing fails', async () => {
@@ -107,7 +109,9 @@ describe('LogViewer', () => {
screen.getByRole('button', { name: /pluginTrigger\.modal\.manual\.logs\.request/ }),
)
expect(screen.getAllByTestId('code-editor')[0]).toHaveTextContent('{invalid}')
expect(JSON.parse((screen.getAllByRole('textbox')[0] as HTMLTextAreaElement).value).data).toBe(
'{invalid}',
)
})
it('should render multiple log entries with distinct indices', () => {
@@ -1,4 +1,3 @@
import type { ComponentProps } from 'react'
import type { AssignerNodeOperation, AssignerNodeType } from '../types'
import { screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
@@ -15,24 +14,6 @@ import NodePreview from '../node'
import Panel from '../panel'
import { AssignerNodeInputType, WriteMode } from '../types'
// Monaco is a browser-owned editor; exercise the surrounding assignment flow with its input contract.
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({
value,
onChange,
readOnly,
}: ComponentProps<
typeof import('@/app/components/workflow/nodes/_base/components/editor/code-editor').default
>) => (
<textarea
aria-label="JSON assignment"
value={typeof value === 'object' ? JSON.stringify(value) : value}
readOnly={readOnly}
onChange={(event) => onChange?.(event.target.value)}
/>
),
}))
const operation = (overrides: Partial<AssignerNodeOperation> = {}): AssignerNodeOperation => ({
variable_selector: ['conversation', 'greeting'],
input_type: AssignerNodeInputType.constant,
@@ -198,7 +179,7 @@ describe('Assigner panel integration', () => {
const { getData } = renderPanel([
operation({ variable_selector: ['conversation', 'settings'], value: '{"a":1}' }),
])
const editor = screen.getByRole('textbox', { name: 'JSON assignment' })
const editor = screen.getByTestId('monaco-editor')
await user.clear(editor)
await user.paste('{"a":2}')
expect(getData().items[0]?.value).toBe('{"a":2}')