feat(tests): add comprehensive tests for Processing and EmbeddingProcess components (#29873)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import ChunkPreview from './chunk-preview'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Mock dataset-detail context - needs mock to control return values
|
||||
const mockDocForm = jest.fn()
|
||||
jest.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (_selector: (s: { dataset: { doc_form: ChunkingMode } }) => ChunkingMode) => {
|
||||
return mockDocForm()
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock document picker - needs mock for simplified interaction testing
|
||||
jest.mock('../../../common/document-picker/preview-document-picker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ files, onChange, value }: {
|
||||
files: Array<{ id: string; name: string; extension: string }>
|
||||
onChange: (selected: { id: string; name: string; extension: string }) => void
|
||||
value: { id: string; name: string; extension: string }
|
||||
}) => (
|
||||
<div data-testid="document-picker">
|
||||
<span data-testid="picker-value">{value?.name || 'No selection'}</span>
|
||||
<select
|
||||
data-testid="picker-select"
|
||||
value={value?.id || ''}
|
||||
onChange={(e) => {
|
||||
const selected = files.find(f => f.id === e.target.value)
|
||||
if (selected)
|
||||
onChange(selected)
|
||||
}}
|
||||
>
|
||||
{files.map(f => (
|
||||
<option key={f.id} value={f.id}>{f.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Test data factories
|
||||
const createMockLocalFile = (overrides?: Partial<CustomFile>): CustomFile => ({
|
||||
id: 'file-1',
|
||||
name: 'test-file.pdf',
|
||||
size: 1024,
|
||||
type: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
lastModified: Date.now(),
|
||||
webkitRelativePath: '',
|
||||
arrayBuffer: jest.fn() as () => Promise<ArrayBuffer>,
|
||||
bytes: jest.fn() as () => Promise<Uint8Array>,
|
||||
slice: jest.fn() as (start?: number, end?: number, contentType?: string) => Blob,
|
||||
stream: jest.fn() as () => ReadableStream<Uint8Array>,
|
||||
text: jest.fn() as () => Promise<string>,
|
||||
...overrides,
|
||||
} as CustomFile)
|
||||
|
||||
const createMockNotionPage = (overrides?: Partial<NotionPage>): NotionPage => ({
|
||||
page_id: 'page-1',
|
||||
page_name: 'Test Page',
|
||||
workspace_id: 'workspace-1',
|
||||
type: 'page',
|
||||
page_icon: null,
|
||||
parent_id: 'parent-1',
|
||||
is_bound: true,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockCrawlResult = (overrides?: Partial<CrawlResultItem>): CrawlResultItem => ({
|
||||
title: 'Test Website',
|
||||
markdown: 'Test content',
|
||||
description: 'Test description',
|
||||
source_url: 'https://example.com',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockOnlineDriveFile = (overrides?: Partial<OnlineDriveFile>): OnlineDriveFile => ({
|
||||
id: 'drive-file-1',
|
||||
name: 'test-drive-file.docx',
|
||||
size: 2048,
|
||||
type: OnlineDriveFileType.file,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockEstimateData = (overrides?: Partial<FileIndexingEstimateResponse>): FileIndexingEstimateResponse => ({
|
||||
total_nodes: 5,
|
||||
tokens: 1000,
|
||||
total_price: 0.01,
|
||||
currency: 'USD',
|
||||
total_segments: 10,
|
||||
preview: [
|
||||
{ content: 'Chunk content 1', child_chunks: ['child 1', 'child 2'] },
|
||||
{ content: 'Chunk content 2', child_chunks: ['child 3'] },
|
||||
],
|
||||
qa_preview: [
|
||||
{ question: 'Q1', answer: 'A1' },
|
||||
{ question: 'Q2', answer: 'A2' },
|
||||
],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
dataSourceType: DatasourceType.localFile,
|
||||
localFiles: [createMockLocalFile()],
|
||||
onlineDocuments: [createMockNotionPage()],
|
||||
websitePages: [createMockCrawlResult()],
|
||||
onlineDriveFiles: [createMockOnlineDriveFile()],
|
||||
isIdle: false,
|
||||
isPending: false,
|
||||
estimateData: undefined,
|
||||
onPreview: jest.fn(),
|
||||
handlePreviewFileChange: jest.fn(),
|
||||
handlePreviewOnlineDocumentChange: jest.fn(),
|
||||
handlePreviewWebsitePageChange: jest.fn(),
|
||||
handlePreviewOnlineDriveFileChange: jest.fn(),
|
||||
}
|
||||
|
||||
describe('ChunkPreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with preview container', () => {
|
||||
render(<ChunkPreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetCreation.stepTwo.preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for local files', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.localFile} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for online documents', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.onlineDocument} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for website pages', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.websiteCrawl} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render document picker for online drive files', () => {
|
||||
render(<ChunkPreview {...defaultProps} dataSourceType={DatasourceType.onlineDrive} />)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render badge with chunk count for non-QA mode', () => {
|
||||
const estimateData = createMockEstimateData({ total_segments: 15 })
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
// Badge shows chunk count via i18n key with count option
|
||||
expect(screen.getByText(/previewChunkCount.*15/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render badge for QA mode', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.qa)
|
||||
const estimateData = createMockEstimateData()
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
// No badge with total_segments
|
||||
expect(screen.queryByText(/10/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Idle State', () => {
|
||||
it('should render idle state with preview tip and button', () => {
|
||||
render(<ChunkPreview {...defaultProps} isIdle={true} />)
|
||||
|
||||
// i18n mock returns keys
|
||||
expect(screen.getByText('datasetCreation.stepTwo.previewChunkTip')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepTwo.previewChunks')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview button is clicked', () => {
|
||||
const onPreview = jest.fn()
|
||||
|
||||
render(<ChunkPreview {...defaultProps} isIdle={true} onPreview={onPreview} />)
|
||||
|
||||
const button = screen.getByRole('button', { name: /previewChunks/i })
|
||||
fireEvent.click(button)
|
||||
expect(onPreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should render skeleton loading when isPending is true', () => {
|
||||
render(<ChunkPreview {...defaultProps} isPending={true} />)
|
||||
|
||||
// Skeleton loading renders multiple skeleton containers
|
||||
expect(document.querySelector('.space-y-6')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render preview content when loading', () => {
|
||||
const estimateData = createMockEstimateData()
|
||||
|
||||
render(<ChunkPreview {...defaultProps} isPending={true} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Chunk content 1')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('QA Mode Preview', () => {
|
||||
it('should render QA preview chunks when doc_form is qa', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.qa)
|
||||
const estimateData = createMockEstimateData({
|
||||
qa_preview: [
|
||||
{ question: 'Question 1?', answer: 'Answer 1' },
|
||||
{ question: 'Question 2?', answer: 'Answer 2' },
|
||||
],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.getByText('Question 1?')).toBeInTheDocument()
|
||||
expect(screen.getByText('Answer 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Question 2?')).toBeInTheDocument()
|
||||
expect(screen.getByText('Answer 2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Text Mode Preview', () => {
|
||||
it('should render text preview chunks when doc_form is text', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
const estimateData = createMockEstimateData({
|
||||
preview: [
|
||||
{ content: 'Text chunk 1', child_chunks: [] },
|
||||
{ content: 'Text chunk 2', child_chunks: [] },
|
||||
],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.getByText('Text chunk 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Text chunk 2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Parent-Child Mode Preview', () => {
|
||||
it('should render parent-child preview chunks', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.parentChild)
|
||||
const estimateData = createMockEstimateData({
|
||||
preview: [
|
||||
{ content: 'Parent chunk 1', child_chunks: ['Child 1', 'Child 2'] },
|
||||
],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.getByText('Child 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Child 2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Document Selection', () => {
|
||||
it('should handle local file selection change', () => {
|
||||
const handlePreviewFileChange = jest.fn()
|
||||
const localFiles = [
|
||||
createMockLocalFile({ id: 'file-1', name: 'file1.pdf' }),
|
||||
createMockLocalFile({ id: 'file-2', name: 'file2.pdf' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.localFile}
|
||||
localFiles={localFiles}
|
||||
handlePreviewFileChange={handlePreviewFileChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'file-2' } })
|
||||
|
||||
expect(handlePreviewFileChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle online document selection change', () => {
|
||||
const handlePreviewOnlineDocumentChange = jest.fn()
|
||||
const onlineDocuments = [
|
||||
createMockNotionPage({ page_id: 'page-1', page_name: 'Page 1' }),
|
||||
createMockNotionPage({ page_id: 'page-2', page_name: 'Page 2' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDocument}
|
||||
onlineDocuments={onlineDocuments}
|
||||
handlePreviewOnlineDocumentChange={handlePreviewOnlineDocumentChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'page-2' } })
|
||||
|
||||
expect(handlePreviewOnlineDocumentChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle website page selection change', () => {
|
||||
const handlePreviewWebsitePageChange = jest.fn()
|
||||
const websitePages = [
|
||||
createMockCrawlResult({ source_url: 'https://example1.com', title: 'Site 1' }),
|
||||
createMockCrawlResult({ source_url: 'https://example2.com', title: 'Site 2' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.websiteCrawl}
|
||||
websitePages={websitePages}
|
||||
handlePreviewWebsitePageChange={handlePreviewWebsitePageChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'https://example2.com' } })
|
||||
|
||||
expect(handlePreviewWebsitePageChange).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle online drive file selection change', () => {
|
||||
const handlePreviewOnlineDriveFileChange = jest.fn()
|
||||
const onlineDriveFiles = [
|
||||
createMockOnlineDriveFile({ id: 'drive-1', name: 'file1.docx' }),
|
||||
createMockOnlineDriveFile({ id: 'drive-2', name: 'file2.docx' }),
|
||||
]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDrive}
|
||||
onlineDriveFiles={onlineDriveFiles}
|
||||
handlePreviewOnlineDriveFileChange={handlePreviewOnlineDriveFileChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
const select = screen.getByTestId('picker-select')
|
||||
fireEvent.change(select, { target: { value: 'drive-2' } })
|
||||
|
||||
expect(handlePreviewOnlineDriveFileChange).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty estimate data', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={undefined} />)
|
||||
|
||||
expect(screen.queryByText('Chunk content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty preview array', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
const estimateData = createMockEstimateData({ preview: [] })
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Chunk content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty qa_preview array', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.qa)
|
||||
const estimateData = createMockEstimateData({ qa_preview: [] })
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Q1')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty child_chunks in parent-child mode', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.parentChild)
|
||||
const estimateData = createMockEstimateData({
|
||||
preview: [{ content: 'Parent', child_chunks: [] }],
|
||||
})
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
expect(screen.queryByText('Child')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle badge showing 0 chunks', () => {
|
||||
mockDocForm.mockReturnValue(ChunkingMode.text)
|
||||
const estimateData = createMockEstimateData({ total_segments: 0 })
|
||||
|
||||
render(<ChunkPreview {...defaultProps} estimateData={estimateData} />)
|
||||
|
||||
// Badge with 0
|
||||
expect(screen.getByText(/0/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined online document properties', () => {
|
||||
const onlineDocuments = [createMockNotionPage({ page_id: '', page_name: '' })]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDocument}
|
||||
onlineDocuments={onlineDocuments}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined website page properties', () => {
|
||||
const websitePages = [createMockCrawlResult({ source_url: '', title: '' })]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.websiteCrawl}
|
||||
websitePages={websitePages}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle undefined online drive file properties', () => {
|
||||
const onlineDriveFiles = [createMockOnlineDriveFile({ id: '', name: '' })]
|
||||
|
||||
render(
|
||||
<ChunkPreview
|
||||
{...defaultProps}
|
||||
dataSourceType={DatasourceType.onlineDrive}
|
||||
onlineDriveFiles={onlineDriveFiles}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('document-picker')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Component Memoization', () => {
|
||||
it('should be exported as a memoized component', () => {
|
||||
// ChunkPreview is wrapped with React.memo
|
||||
// We verify this by checking the component type
|
||||
expect(typeof ChunkPreview).toBe('object')
|
||||
expect(ChunkPreview.$$typeof?.toString()).toBe('Symbol(react.memo)')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,320 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import FilePreview from './file-preview'
|
||||
import type { CustomFile as File } from '@/models/datasets'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Mock useFilePreview hook - needs to be mocked to control return values
|
||||
const mockUseFilePreview = jest.fn()
|
||||
jest.mock('@/service/use-common', () => ({
|
||||
useFilePreview: (fileID: string) => mockUseFilePreview(fileID),
|
||||
}))
|
||||
|
||||
// Test data factory
|
||||
const createMockFile = (overrides?: Partial<File>): File => ({
|
||||
id: 'file-123',
|
||||
name: 'test-document.pdf',
|
||||
size: 2048,
|
||||
type: 'application/pdf',
|
||||
extension: 'pdf',
|
||||
lastModified: Date.now(),
|
||||
webkitRelativePath: '',
|
||||
arrayBuffer: jest.fn() as () => Promise<ArrayBuffer>,
|
||||
bytes: jest.fn() as () => Promise<Uint8Array>,
|
||||
slice: jest.fn() as (start?: number, end?: number, contentType?: string) => Blob,
|
||||
stream: jest.fn() as () => ReadableStream<Uint8Array>,
|
||||
text: jest.fn() as () => Promise<string>,
|
||||
...overrides,
|
||||
} as File)
|
||||
|
||||
const createMockFilePreviewData = (content: string = 'This is the file content') => ({
|
||||
content,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
file: createMockFile(),
|
||||
hidePreview: jest.fn(),
|
||||
}
|
||||
|
||||
describe('FilePreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with file information', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepOne.preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('test-document.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display file extension in uppercase via CSS class', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// The extension is displayed in the info section (as uppercase via CSS class)
|
||||
const extensionElement = screen.getByText('pdf')
|
||||
expect(extensionElement).toBeInTheDocument()
|
||||
expect(extensionElement).toHaveClass('uppercase')
|
||||
})
|
||||
|
||||
it('should display formatted file size', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatFileSize: 2048 bytes => "2.00 KB"
|
||||
expect(screen.getByText('2.00 KB')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call useFilePreview with correct fileID', () => {
|
||||
const file = createMockFile({ id: 'specific-file-id' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(mockUseFilePreview).toHaveBeenCalledWith('specific-file-id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('File Name Processing', () => {
|
||||
it('should extract file name without extension', () => {
|
||||
const file = createMockFile({ name: 'my-document.pdf', extension: 'pdf' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// The displayed text is `${fileName}.${extension}`, where fileName is name without ext
|
||||
// my-document.pdf -> fileName = 'my-document', displayed as 'my-document.pdf'
|
||||
expect(screen.getByText('my-document.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle file name with multiple dots', () => {
|
||||
const file = createMockFile({ name: 'my.file.name.pdf', extension: 'pdf' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// fileName = arr.slice(0, -1).join() = 'my,file,name', then displayed as 'my,file,name.pdf'
|
||||
expect(screen.getByText('my,file,name.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty file name', () => {
|
||||
const file = createMockFile({ name: '', extension: '' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// fileName = '', displayed as '.'
|
||||
expect(screen.getByText('.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle file without extension in name', () => {
|
||||
const file = createMockFile({ name: 'noextension', extension: '' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// fileName = '' (slice returns empty for single element array), displayed as '.'
|
||||
expect(screen.getByText('.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should render loading component when fetching', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: true,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Loading component renders skeleton
|
||||
expect(document.querySelector('.overflow-hidden')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render content when loading', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData('Some content'),
|
||||
isFetching: true,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.queryByText('Some content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Display', () => {
|
||||
it('should render file content when loaded', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData('This is the file content'),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('This is the file content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display character count when data is available', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData('Hello'), // 5 characters
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated returns "5" for numbers < 1000
|
||||
expect(screen.getByText(/5/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format large character counts', () => {
|
||||
const longContent = 'a'.repeat(2500)
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData(longContent),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated uses lowercase 'k': "2.5k"
|
||||
expect(screen.getByText(/2\.5k/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not display character count when data is not available', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// No character text shown
|
||||
expect(screen.queryByText(/datasetPipeline\.addDocuments\.characters/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = jest.fn()
|
||||
|
||||
render(<FilePreview {...defaultProps} hidePreview={hidePreview} />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('File Size Formatting', () => {
|
||||
it('should format small file sizes in bytes', () => {
|
||||
const file = createMockFile({ size: 500 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize: 500 => "500.00 bytes"
|
||||
expect(screen.getByText('500.00 bytes')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format kilobyte file sizes', () => {
|
||||
const file = createMockFile({ size: 5120 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize: 5120 => "5.00 KB"
|
||||
expect(screen.getByText('5.00 KB')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format megabyte file sizes', () => {
|
||||
const file = createMockFile({ size: 2097152 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize: 2097152 => "2.00 MB"
|
||||
expect(screen.getByText('2.00 MB')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined file id', () => {
|
||||
const file = createMockFile({ id: undefined })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(mockUseFilePreview).toHaveBeenCalledWith('')
|
||||
})
|
||||
|
||||
it('should handle empty extension', () => {
|
||||
const file = createMockFile({ extension: undefined })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle zero file size', () => {
|
||||
const file = createMockFile({ size: 0 })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Real formatFileSize returns 0 for falsy values
|
||||
// The component still renders
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long file content', () => {
|
||||
const veryLongContent = 'a'.repeat(1000000)
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData(veryLongContent),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated: 1000000 => "1M"
|
||||
expect(screen.getByText(/1M/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty content', () => {
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: createMockFilePreviewData(''),
|
||||
isFetching: false,
|
||||
})
|
||||
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
|
||||
// Real formatNumberAbbreviated: 0 => "0"
|
||||
// Find the element that contains character count info
|
||||
expect(screen.getByText(/0 datasetPipeline/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMemo for fileName', () => {
|
||||
it('should extract file name when file exists', () => {
|
||||
// When file exists, it should extract the name without extension
|
||||
const file = createMockFile({ name: 'document.txt', extension: 'txt' })
|
||||
|
||||
render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByText('document.txt')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should memoize fileName based on file prop', () => {
|
||||
const file = createMockFile({ name: 'test.pdf', extension: 'pdf' })
|
||||
|
||||
const { rerender } = render(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
// Same file should produce same result
|
||||
rerender(<FilePreview {...defaultProps} file={file} />)
|
||||
|
||||
expect(screen.getByText('test.pdf')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,359 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import OnlineDocumentPreview from './online-document-preview'
|
||||
import type { NotionPage } from '@/models/common'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Spy on Toast.notify
|
||||
const toastNotifySpy = jest.spyOn(Toast, 'notify')
|
||||
|
||||
// Mock dataset-detail context - needs mock to control return values
|
||||
const mockPipelineId = jest.fn()
|
||||
jest.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (_selector: (s: { dataset: { pipeline_id: string } }) => string) => {
|
||||
return mockPipelineId()
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock usePreviewOnlineDocument hook - needs mock to control mutation behavior
|
||||
const mockMutateAsync = jest.fn()
|
||||
const mockUsePreviewOnlineDocument = jest.fn()
|
||||
jest.mock('@/service/use-pipeline', () => ({
|
||||
usePreviewOnlineDocument: () => mockUsePreviewOnlineDocument(),
|
||||
}))
|
||||
|
||||
// Mock data source store - needs mock to control store state
|
||||
const mockCurrentCredentialId = 'credential-123'
|
||||
const mockGetState = jest.fn(() => ({
|
||||
currentCredentialId: mockCurrentCredentialId,
|
||||
}))
|
||||
jest.mock('../data-source/store', () => ({
|
||||
useDataSourceStore: () => ({
|
||||
getState: mockGetState,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Test data factory
|
||||
const createMockNotionPage = (overrides?: Partial<NotionPage>): NotionPage => ({
|
||||
page_id: 'page-123',
|
||||
page_name: 'Test Notion Page',
|
||||
workspace_id: 'workspace-456',
|
||||
type: 'page',
|
||||
page_icon: null,
|
||||
parent_id: 'parent-789',
|
||||
is_bound: true,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
currentPage: createMockNotionPage(),
|
||||
datasourceNodeId: 'datasource-node-123',
|
||||
hidePreview: jest.fn(),
|
||||
}
|
||||
|
||||
describe('OnlineDocumentPreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPipelineId.mockReturnValue('pipeline-123')
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
})
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Test content' })
|
||||
return Promise.resolve({ content: 'Test content' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with page information', () => {
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepOne.preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Notion Page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display page type', () => {
|
||||
const currentPage = createMockNotionPage({ type: 'database' })
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
||||
|
||||
expect(screen.getByText('database')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Data Fetching', () => {
|
||||
it('should call mutateAsync with correct parameters on mount', async () => {
|
||||
const currentPage = createMockNotionPage({
|
||||
workspace_id: 'ws-123',
|
||||
page_id: 'pg-456',
|
||||
type: 'page',
|
||||
})
|
||||
|
||||
render(
|
||||
<OnlineDocumentPreview
|
||||
{...defaultProps}
|
||||
currentPage={currentPage}
|
||||
datasourceNodeId="node-789"
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
{
|
||||
workspaceID: 'ws-123',
|
||||
pageID: 'pg-456',
|
||||
pageType: 'page',
|
||||
pipelineId: 'pipeline-123',
|
||||
datasourceNodeId: 'node-789',
|
||||
credentialId: mockCurrentCredentialId,
|
||||
},
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should fetch data again when page_id changes', async () => {
|
||||
const currentPage1 = createMockNotionPage({ page_id: 'page-1' })
|
||||
const currentPage2 = createMockNotionPage({ page_id: 'page-2' })
|
||||
|
||||
const { rerender } = render(
|
||||
<OnlineDocumentPreview {...defaultProps} currentPage={currentPage1} />,
|
||||
)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
rerender(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage2} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty pipelineId', async () => {
|
||||
mockPipelineId.mockReturnValue(undefined)
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pipelineId: '',
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Loading State', () => {
|
||||
it('should render loading component when isPending is true', () => {
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// Loading component renders skeleton
|
||||
expect(document.querySelector('.overflow-hidden')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render markdown content when loading', () => {
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: true,
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// Content area should not be present
|
||||
expect(screen.queryByText('Test content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Display', () => {
|
||||
it('should render markdown content when loaded', async () => {
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Markdown content here' })
|
||||
return Promise.resolve({ content: 'Markdown content here' })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
// Markdown component renders the content
|
||||
const contentArea = document.querySelector('.overflow-hidden.px-6.py-5')
|
||||
expect(contentArea).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should display character count', async () => {
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Hello' }) // 5 characters
|
||||
return Promise.resolve({ content: 'Hello' })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
// Real formatNumberAbbreviated returns "5" for numbers < 1000
|
||||
expect(screen.getByText(/5/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should format large character counts', async () => {
|
||||
const longContent = 'a'.repeat(2500)
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: longContent })
|
||||
return Promise.resolve({ content: longContent })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
// Real formatNumberAbbreviated uses lowercase 'k': "2.5k"
|
||||
expect(screen.getByText(/2\.5k/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should show character count based on fetched content', async () => {
|
||||
// When content is set via onSuccess, character count is displayed
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: 'Test content' }) // 12 characters
|
||||
return Promise.resolve({ content: 'Test content' })
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/12/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should show toast notification on error', async () => {
|
||||
const errorMessage = 'Failed to fetch document'
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onError(new Error(errorMessage))
|
||||
// Return a resolved promise to avoid unhandled rejection
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: errorMessage,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
const networkError = new Error('Network Error')
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onError(networkError)
|
||||
// Return a resolved promise to avoid unhandled rejection
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toastNotifySpy).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'Network Error',
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = jest.fn()
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} hidePreview={hidePreview} />)
|
||||
|
||||
// Find the close button in the header area (not toast buttons)
|
||||
const headerArea = document.querySelector('.flex.gap-x-2.border-b')
|
||||
const closeButton = headerArea?.querySelector('button')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined page_name', () => {
|
||||
const currentPage = createMockNotionPage({ page_name: '' })
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
||||
|
||||
// Find the close button in the header area
|
||||
const headerArea = document.querySelector('.flex.gap-x-2.border-b')
|
||||
const closeButton = headerArea?.querySelector('button')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle different page types', () => {
|
||||
const currentPage = createMockNotionPage({ type: 'database' })
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
||||
|
||||
expect(screen.getByText('database')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use credentialId from store', async () => {
|
||||
mockGetState.mockReturnValue({
|
||||
currentCredentialId: 'custom-credential',
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockMutateAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
credentialId: 'custom-credential',
|
||||
}),
|
||||
expect.anything(),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render markdown content when content is empty and not pending', async () => {
|
||||
mockMutateAsync.mockImplementation((params, callbacks) => {
|
||||
callbacks.onSuccess({ content: '' })
|
||||
return Promise.resolve({ content: '' })
|
||||
})
|
||||
mockUsePreviewOnlineDocument.mockReturnValue({
|
||||
mutateAsync: mockMutateAsync,
|
||||
isPending: false,
|
||||
})
|
||||
|
||||
render(<OnlineDocumentPreview {...defaultProps} />)
|
||||
|
||||
// Content is empty, markdown area should still render but be empty
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Test content')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,256 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import React from 'react'
|
||||
import WebsitePreview from './web-preview'
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
|
||||
// Uses __mocks__/react-i18next.ts automatically
|
||||
|
||||
// Test data factory
|
||||
const createMockCrawlResult = (overrides?: Partial<CrawlResultItem>): CrawlResultItem => ({
|
||||
title: 'Test Website Title',
|
||||
markdown: 'This is the **markdown** content of the website.',
|
||||
description: 'Test description',
|
||||
source_url: 'https://example.com/page',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
currentWebsite: createMockCrawlResult(),
|
||||
hidePreview: jest.fn(),
|
||||
}
|
||||
|
||||
describe('WebsitePreview', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Rendering', () => {
|
||||
it('should render the component with website information', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
// i18n mock returns key by default
|
||||
expect(screen.getByText('datasetPipeline.addDocuments.stepOne.preview')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test Website Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display the source URL', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('https://example.com/page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the markdown content', () => {
|
||||
render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
expect(screen.getByText('This is the **markdown** content of the website.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Character Count', () => {
|
||||
it('should display character count for small content', () => {
|
||||
const currentWebsite = createMockCrawlResult({ markdown: 'Hello' }) // 5 characters
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
// Real formatNumberAbbreviated returns "5" for numbers < 1000
|
||||
expect(screen.getByText(/5/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format character count in thousands', () => {
|
||||
const longContent = 'a'.repeat(2500)
|
||||
const currentWebsite = createMockCrawlResult({ markdown: longContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
// Real formatNumberAbbreviated uses lowercase 'k': "2.5k"
|
||||
expect(screen.getByText(/2\.5k/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should format character count in millions', () => {
|
||||
const veryLongContent = 'a'.repeat(1500000)
|
||||
const currentWebsite = createMockCrawlResult({ markdown: veryLongContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(/1\.5M/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show 0 characters for empty markdown', () => {
|
||||
const currentWebsite = createMockCrawlResult({ markdown: '' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(/0/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = jest.fn()
|
||||
|
||||
render(<WebsitePreview {...defaultProps} hidePreview={hidePreview} />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('URL Display', () => {
|
||||
it('should display long URLs', () => {
|
||||
const longUrl = 'https://example.com/very/long/path/to/page/with/many/segments'
|
||||
const currentWebsite = createMockCrawlResult({ source_url: longUrl })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
const urlElement = screen.getByTitle(longUrl)
|
||||
expect(urlElement).toBeInTheDocument()
|
||||
expect(urlElement).toHaveTextContent(longUrl)
|
||||
})
|
||||
|
||||
it('should display URL with title attribute', () => {
|
||||
const currentWebsite = createMockCrawlResult({ source_url: 'https://test.com' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByTitle('https://test.com')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Content Display', () => {
|
||||
it('should display the markdown content in content area', () => {
|
||||
const currentWebsite = createMockCrawlResult({
|
||||
markdown: 'Content with **bold** and *italic* text.',
|
||||
})
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText('Content with **bold** and *italic* text.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle multiline content', () => {
|
||||
const multilineContent = 'Line 1\nLine 2\nLine 3'
|
||||
const currentWebsite = createMockCrawlResult({ markdown: multilineContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
// Multiline content is rendered as-is
|
||||
expect(screen.getByText((content) => {
|
||||
return content.includes('Line 1') && content.includes('Line 2') && content.includes('Line 3')
|
||||
})).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in content', () => {
|
||||
const specialContent = '<script>alert("xss")</script> & < > " \''
|
||||
const currentWebsite = createMockCrawlResult({ markdown: specialContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(specialContent)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty title', () => {
|
||||
const currentWebsite = createMockCrawlResult({ title: '' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty source URL', () => {
|
||||
const currentWebsite = createMockCrawlResult({ source_url: '' })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long title', () => {
|
||||
const longTitle = 'A'.repeat(500)
|
||||
const currentWebsite = createMockCrawlResult({ title: longTitle })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(longTitle)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle unicode characters in content', () => {
|
||||
const unicodeContent = '你好世界 🌍 مرحبا こんにちは'
|
||||
const currentWebsite = createMockCrawlResult({ markdown: unicodeContent })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByText(unicodeContent)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle URL with query parameters', () => {
|
||||
const urlWithParams = 'https://example.com/page?query=test¶m=value'
|
||||
const currentWebsite = createMockCrawlResult({ source_url: urlWithParams })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByTitle(urlWithParams)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle URL with hash fragment', () => {
|
||||
const urlWithHash = 'https://example.com/page#section-1'
|
||||
const currentWebsite = createMockCrawlResult({ source_url: urlWithHash })
|
||||
|
||||
render(<WebsitePreview {...defaultProps} currentWebsite={currentWebsite} />)
|
||||
|
||||
expect(screen.getByTitle(urlWithHash)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Styling', () => {
|
||||
it('should apply container styles', () => {
|
||||
const { container } = render(<WebsitePreview {...defaultProps} />)
|
||||
|
||||
const mainContainer = container.firstChild as HTMLElement
|
||||
expect(mainContainer).toHaveClass('flex', 'h-full', 'w-full', 'flex-col')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Multiple Renders', () => {
|
||||
it('should update when currentWebsite changes', () => {
|
||||
const website1 = createMockCrawlResult({ title: 'Website 1', markdown: 'Content 1' })
|
||||
const website2 = createMockCrawlResult({ title: 'Website 2', markdown: 'Content 2' })
|
||||
|
||||
const { rerender } = render(<WebsitePreview {...defaultProps} currentWebsite={website1} />)
|
||||
|
||||
expect(screen.getByText('Website 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Content 1')).toBeInTheDocument()
|
||||
|
||||
rerender(<WebsitePreview {...defaultProps} currentWebsite={website2} />)
|
||||
|
||||
expect(screen.getByText('Website 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Content 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call new hidePreview when prop changes', () => {
|
||||
const hidePreview1 = jest.fn()
|
||||
const hidePreview2 = jest.fn()
|
||||
|
||||
const { rerender } = render(<WebsitePreview {...defaultProps} hidePreview={hidePreview1} />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
expect(hidePreview1).toHaveBeenCalledTimes(1)
|
||||
|
||||
rerender(<WebsitePreview {...defaultProps} hidePreview={hidePreview2} />)
|
||||
|
||||
fireEvent.click(closeButton)
|
||||
expect(hidePreview2).toHaveBeenCalledTimes(1)
|
||||
expect(hidePreview1).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user