mirror of
https://github.com/langgenius/dify.git
synced 2026-11-10 21:35:45 +08:00
test(web): replace AP30/AP32 render doubles with real behavior (#45440)
This commit is contained in:
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()
|
||||
})
|
||||
})
|
||||
+9
-10
@@ -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()
|
||||
})
|
||||
})
|
||||
+239
-639
@@ -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()
|
||||
})
|
||||
})
|
||||
+18
-14
@@ -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}')
|
||||
|
||||
Reference in new issue
Block a user