mirror of
https://github.com/langgenius/dify.git
synced 2026-04-26 21:55:58 +08:00
test: add unit tests for website crawl and document preview components
- Introduced new test files for CheckboxWithLabel, CrawledResultItem, ErrorMessage, and various components related to website crawling and document preview. - Enhanced test coverage by validating rendering, user interactions, and edge cases for each component. - Ensured proper functionality of user interactions such as checkbox selections, button clicks, and rendering of dynamic content. These additions improve the reliability and maintainability of the website crawl and document preview features.
This commit is contained in:
@ -0,0 +1,176 @@
|
||||
import type { ParentChildConfig } from '../hooks'
|
||||
import type { FileIndexingEstimateResponse } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode, DataSourceType } from '@/models/datasets'
|
||||
import { PreviewPanel } from './preview-panel'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: { count?: number }) => opts?.count !== undefined ? `${key}-${opts.count}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiSearchEyeLine: () => <span data-testid="search-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/float-right-container', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div data-testid="float-container">{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/badge', () => ({
|
||||
default: ({ text }: { text: string }) => <span data-testid="badge">{text}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/skeleton', () => ({
|
||||
SkeletonContainer: ({ children }: { children: React.ReactNode }) => <div data-testid="skeleton">{children}</div>,
|
||||
SkeletonPoint: () => <span />,
|
||||
SkeletonRectangle: () => <span />,
|
||||
SkeletonRow: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../chunk', () => ({
|
||||
ChunkContainer: ({ children, label }: { children: React.ReactNode, label: string }) => (
|
||||
<div data-testid="chunk-container">
|
||||
{label}
|
||||
:
|
||||
{' '}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
QAPreview: ({ qa }: { qa: { question: string } }) => <div data-testid="qa-preview">{qa.question}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../common/document-picker/preview-document-picker', () => ({
|
||||
default: () => <div data-testid="doc-picker" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../../documents/detail/completed/common/summary-label', () => ({
|
||||
default: ({ summary }: { summary: string }) => <span data-testid="summary">{summary}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../formatted-text/flavours/preview-slice', () => ({
|
||||
PreviewSlice: ({ label, text }: { label: string, text: string }) => (
|
||||
<span data-testid="preview-slice">
|
||||
{label}
|
||||
:
|
||||
{' '}
|
||||
{text}
|
||||
</span>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../formatted-text/formatted', () => ({
|
||||
FormattedText: ({ children }: { children: React.ReactNode }) => <p data-testid="formatted-text">{children}</p>,
|
||||
}))
|
||||
|
||||
vi.mock('../../../preview/container', () => ({
|
||||
default: ({ children, header }: { children: React.ReactNode, header: React.ReactNode }) => (
|
||||
<div data-testid="preview-container">
|
||||
{header}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../../preview/header', () => ({
|
||||
PreviewHeader: ({ children, title }: { children: React.ReactNode, title: string }) => (
|
||||
<div data-testid="preview-header">
|
||||
{title}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
FULL_DOC_PREVIEW_LENGTH: 3,
|
||||
}))
|
||||
|
||||
describe('PreviewPanel', () => {
|
||||
const defaultProps = {
|
||||
isMobile: false,
|
||||
dataSourceType: DataSourceType.FILE,
|
||||
currentDocForm: ChunkingMode.text,
|
||||
parentChildConfig: { chunkForContext: 'paragraph' } as ParentChildConfig,
|
||||
pickerFiles: [{ id: '1', name: 'file.pdf', extension: 'pdf' }],
|
||||
pickerValue: { id: '1', name: 'file.pdf', extension: 'pdf' },
|
||||
isIdle: false,
|
||||
isPending: false,
|
||||
onPickerChange: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render preview header with title', () => {
|
||||
render(<PreviewPanel {...defaultProps} />)
|
||||
expect(screen.getByTestId('preview-header')).toHaveTextContent('stepTwo.preview')
|
||||
})
|
||||
|
||||
it('should render document picker', () => {
|
||||
render(<PreviewPanel {...defaultProps} />)
|
||||
expect(screen.getByTestId('doc-picker')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show idle state when isIdle is true', () => {
|
||||
render(<PreviewPanel {...defaultProps} isIdle={true} />)
|
||||
expect(screen.getByText('stepTwo.previewChunkTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading skeletons when isPending', () => {
|
||||
render(<PreviewPanel {...defaultProps} isPending={true} />)
|
||||
expect(screen.getAllByTestId('skeleton')).toHaveLength(10)
|
||||
})
|
||||
|
||||
it('should render text preview chunks', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = {
|
||||
total_segments: 2,
|
||||
preview: [
|
||||
{ content: 'chunk 1 text', child_chunks: [], summary: '' },
|
||||
{ content: 'chunk 2 text', child_chunks: [], summary: 'summary text' },
|
||||
],
|
||||
}
|
||||
render(<PreviewPanel {...defaultProps} estimate={estimate as FileIndexingEstimateResponse} />)
|
||||
expect(screen.getAllByTestId('chunk-container')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render QA preview', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = {
|
||||
qa_preview: [
|
||||
{ question: 'Q1', answer: 'A1' },
|
||||
],
|
||||
}
|
||||
render(
|
||||
<PreviewPanel
|
||||
{...defaultProps}
|
||||
currentDocForm={ChunkingMode.qa}
|
||||
estimate={estimate as FileIndexingEstimateResponse}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('qa-preview')).toHaveTextContent('Q1')
|
||||
})
|
||||
|
||||
it('should render parent-child preview', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = {
|
||||
preview: [
|
||||
{ content: 'parent chunk', child_chunks: ['child1', 'child2'], summary: '' },
|
||||
],
|
||||
}
|
||||
render(
|
||||
<PreviewPanel
|
||||
{...defaultProps}
|
||||
currentDocForm={ChunkingMode.parentChild}
|
||||
estimate={estimate as FileIndexingEstimateResponse}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getAllByTestId('preview-slice')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should show badge with chunk count for non-QA mode', () => {
|
||||
const estimate: Partial<FileIndexingEstimateResponse> = { total_segments: 5, preview: [] }
|
||||
render(<PreviewPanel {...defaultProps} estimate={estimate as FileIndexingEstimateResponse} />)
|
||||
expect(screen.getByTestId('badge')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,237 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DocumentActionType } from '@/models/datasets'
|
||||
import { useDocumentActions } from './use-document-actions'
|
||||
|
||||
const mockArchive = vi.fn()
|
||||
const mockSummary = vi.fn()
|
||||
const mockEnable = vi.fn()
|
||||
const mockDisable = vi.fn()
|
||||
const mockDelete = vi.fn()
|
||||
const mockRetryIndex = vi.fn()
|
||||
const mockDownloadZip = vi.fn()
|
||||
let mockIsDownloadingZip = false
|
||||
|
||||
vi.mock('@/service/knowledge/use-document', () => ({
|
||||
useDocumentArchive: () => ({ mutateAsync: mockArchive }),
|
||||
useDocumentSummary: () => ({ mutateAsync: mockSummary }),
|
||||
useDocumentEnable: () => ({ mutateAsync: mockEnable }),
|
||||
useDocumentDisable: () => ({ mutateAsync: mockDisable }),
|
||||
useDocumentDelete: () => ({ mutateAsync: mockDelete }),
|
||||
useDocumentBatchRetryIndex: () => ({ mutateAsync: mockRetryIndex }),
|
||||
useDocumentDownloadZip: () => ({ mutateAsync: mockDownloadZip, isPending: mockIsDownloadingZip }),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockToastNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: { notify: (...args: unknown[]) => mockToastNotify(...args) },
|
||||
}))
|
||||
|
||||
const mockDownloadBlob = vi.fn()
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
|
||||
}))
|
||||
|
||||
describe('useDocumentActions', () => {
|
||||
const defaultOptions = {
|
||||
datasetId: 'ds-1',
|
||||
selectedIds: ['doc-1', 'doc-2'],
|
||||
downloadableSelectedIds: ['doc-1'],
|
||||
onUpdate: vi.fn(),
|
||||
onClearSelection: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsDownloadingZip = false
|
||||
})
|
||||
|
||||
it('should return expected functions and state', () => {
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
expect(result.current.handleAction).toBeInstanceOf(Function)
|
||||
expect(result.current.handleBatchReIndex).toBeInstanceOf(Function)
|
||||
expect(result.current.handleBatchDownload).toBeInstanceOf(Function)
|
||||
expect(typeof result.current.isDownloadingZip).toBe('boolean')
|
||||
})
|
||||
|
||||
describe('handleAction', () => {
|
||||
it('should call archive API and show success toast', async () => {
|
||||
mockArchive.mockResolvedValue({ result: 'success' })
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAction(DocumentActionType.archive)()
|
||||
})
|
||||
|
||||
expect(mockArchive).toHaveBeenCalledWith({
|
||||
datasetId: 'ds-1',
|
||||
documentIds: ['doc-1', 'doc-2'],
|
||||
})
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'success' }),
|
||||
)
|
||||
expect(defaultOptions.onUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call enable API on enable action', async () => {
|
||||
mockEnable.mockResolvedValue({ result: 'success' })
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAction(DocumentActionType.enable)()
|
||||
})
|
||||
|
||||
expect(mockEnable).toHaveBeenCalledWith({
|
||||
datasetId: 'ds-1',
|
||||
documentIds: ['doc-1', 'doc-2'],
|
||||
})
|
||||
expect(defaultOptions.onUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call disable API on disable action', async () => {
|
||||
mockDisable.mockResolvedValue({ result: 'success' })
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAction(DocumentActionType.disable)()
|
||||
})
|
||||
|
||||
expect(mockDisable).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call summary API on summary action', async () => {
|
||||
mockSummary.mockResolvedValue({ result: 'success' })
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAction(DocumentActionType.summary)()
|
||||
})
|
||||
|
||||
expect(mockSummary).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onClearSelection on delete action success', async () => {
|
||||
mockDelete.mockResolvedValue({ result: 'success' })
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAction(DocumentActionType.delete)()
|
||||
})
|
||||
|
||||
expect(mockDelete).toHaveBeenCalled()
|
||||
expect(defaultOptions.onClearSelection).toHaveBeenCalled()
|
||||
expect(defaultOptions.onUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not call onClearSelection on non-delete action success', async () => {
|
||||
mockArchive.mockResolvedValue({ result: 'success' })
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAction(DocumentActionType.archive)()
|
||||
})
|
||||
|
||||
expect(defaultOptions.onClearSelection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error toast on action failure', async () => {
|
||||
mockArchive.mockRejectedValue(new Error('fail'))
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleAction(DocumentActionType.archive)()
|
||||
})
|
||||
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
expect(defaultOptions.onUpdate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleBatchReIndex', () => {
|
||||
it('should call retry index API and show success toast', async () => {
|
||||
mockRetryIndex.mockResolvedValue({ result: 'success' })
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleBatchReIndex()
|
||||
})
|
||||
|
||||
expect(mockRetryIndex).toHaveBeenCalledWith({
|
||||
datasetId: 'ds-1',
|
||||
documentIds: ['doc-1', 'doc-2'],
|
||||
})
|
||||
expect(defaultOptions.onClearSelection).toHaveBeenCalled()
|
||||
expect(defaultOptions.onUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error toast on reindex failure', async () => {
|
||||
mockRetryIndex.mockRejectedValue(new Error('fail'))
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleBatchReIndex()
|
||||
})
|
||||
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleBatchDownload', () => {
|
||||
it('should download blob on success', async () => {
|
||||
const blob = new Blob(['test'])
|
||||
mockDownloadZip.mockResolvedValue(blob)
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleBatchDownload()
|
||||
})
|
||||
|
||||
expect(mockDownloadZip).toHaveBeenCalledWith({
|
||||
datasetId: 'ds-1',
|
||||
documentIds: ['doc-1'],
|
||||
})
|
||||
expect(mockDownloadBlob).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: blob,
|
||||
fileName: expect.stringContaining('-docs.zip'),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('should show error toast on download failure', async () => {
|
||||
mockDownloadZip.mockRejectedValue(new Error('fail'))
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleBatchDownload()
|
||||
})
|
||||
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
})
|
||||
|
||||
it('should show error toast when blob is null', async () => {
|
||||
mockDownloadZip.mockResolvedValue(null)
|
||||
const { result } = renderHook(() => useDocumentActions(defaultOptions))
|
||||
|
||||
await act(async () => {
|
||||
await result.current.handleBatchDownload()
|
||||
})
|
||||
|
||||
expect(mockToastNotify).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'error' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,658 +1,64 @@
|
||||
import type { DataSourceCredential } from '@/types/pipeline'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Header from './header'
|
||||
|
||||
// Mock CredentialTypeEnum to avoid deep import chain issues
|
||||
enum MockCredentialTypeEnum {
|
||||
OAUTH2 = 'oauth2',
|
||||
API_KEY = 'api_key',
|
||||
}
|
||||
|
||||
// Mock plugin-auth module to avoid deep import chain issues
|
||||
vi.mock('@/app/components/plugins/plugin-auth', () => ({
|
||||
CredentialTypeEnum: {
|
||||
OAUTH2: 'oauth2',
|
||||
API_KEY: 'api_key',
|
||||
},
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: { pluginName?: string }) => opts?.pluginName ? `${key}-${opts.pluginName}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock portal-to-follow-elem - required for CredentialSelector
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => {
|
||||
const MockPortalToFollowElem = ({ children, open }: any) => {
|
||||
return (
|
||||
<div data-testid="portal-root" data-open={open}>
|
||||
{React.Children.map(children, (child: any) => {
|
||||
if (!child)
|
||||
return null
|
||||
return React.cloneElement(child, { __portalOpen: open })
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiBookOpenLine: () => <span data-testid="book-icon" />,
|
||||
RiEqualizer2Line: ({ onClick }: { onClick?: () => void }) => <span data-testid="config-icon" onClick={onClick} />,
|
||||
}))
|
||||
|
||||
const MockPortalToFollowElemTrigger = ({ children, onClick, className, __portalOpen }: any) => (
|
||||
<div data-testid="portal-trigger" onClick={onClick} className={className} data-open={__portalOpen}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
|
||||
}))
|
||||
|
||||
const MockPortalToFollowElemContent = ({ children, className, __portalOpen }: any) => {
|
||||
if (!__portalOpen)
|
||||
return null
|
||||
return (
|
||||
<div data-testid="portal-content" className={className}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
vi.mock('@/app/components/base/divider', () => ({
|
||||
default: () => <span data-testid="divider" />,
|
||||
}))
|
||||
|
||||
return {
|
||||
PortalToFollowElem: MockPortalToFollowElem,
|
||||
PortalToFollowElemTrigger: MockPortalToFollowElemTrigger,
|
||||
PortalToFollowElemContent: MockPortalToFollowElemContent,
|
||||
}
|
||||
})
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div data-testid="tooltip">{children}</div>,
|
||||
}))
|
||||
|
||||
// ==========================================
|
||||
// Test Data Builders
|
||||
// ==========================================
|
||||
const createMockCredential = (overrides?: Partial<DataSourceCredential>): DataSourceCredential => ({
|
||||
id: 'cred-1',
|
||||
name: 'Test Credential',
|
||||
avatar_url: 'https://example.com/avatar.png',
|
||||
credential: { key: 'value' },
|
||||
is_default: false,
|
||||
type: MockCredentialTypeEnum.OAUTH2 as unknown as DataSourceCredential['type'],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createMockCredentials = (count: number = 3): DataSourceCredential[] =>
|
||||
Array.from({ length: count }, (_, i) =>
|
||||
createMockCredential({
|
||||
id: `cred-${i + 1}`,
|
||||
name: `Credential ${i + 1}`,
|
||||
avatar_url: `https://example.com/avatar-${i + 1}.png`,
|
||||
is_default: i === 0,
|
||||
}))
|
||||
|
||||
type HeaderProps = React.ComponentProps<typeof Header>
|
||||
|
||||
const createDefaultProps = (overrides?: Partial<HeaderProps>): HeaderProps => ({
|
||||
docTitle: 'Documentation',
|
||||
docLink: 'https://docs.example.com',
|
||||
pluginName: 'Test Plugin',
|
||||
currentCredentialId: 'cred-1',
|
||||
onCredentialChange: vi.fn(),
|
||||
credentials: createMockCredentials(),
|
||||
...overrides,
|
||||
})
|
||||
vi.mock('./credential-selector', () => ({
|
||||
default: () => <div data-testid="credential-selector" />,
|
||||
}))
|
||||
|
||||
describe('Header', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const defaultProps = {
|
||||
docTitle: 'Documentation',
|
||||
docLink: 'https://docs.example.com',
|
||||
onClickConfiguration: vi.fn(),
|
||||
pluginName: 'TestPlugin',
|
||||
credentials: [],
|
||||
currentCredentialId: '',
|
||||
onCredentialChange: vi.fn(),
|
||||
}
|
||||
|
||||
it('should render doc link with title', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByText('Documentation')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Rendering Tests
|
||||
// ==========================================
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Documentation')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render documentation link with correct attributes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
docTitle: 'API Docs',
|
||||
docLink: 'https://api.example.com/docs',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link', { name: /API Docs/i })
|
||||
expect(link).toHaveAttribute('href', 'https://api.example.com/docs')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
|
||||
it('should render document title with title attribute', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docTitle: 'My Documentation' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const titleSpan = screen.getByText('My Documentation')
|
||||
expect(titleSpan).toHaveAttribute('title', 'My Documentation')
|
||||
})
|
||||
|
||||
it('should render CredentialSelector with correct props', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - CredentialSelector should render current credential name
|
||||
expect(screen.getByText('Credential 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render configuration button', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render book icon in documentation link', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - RiBookOpenLine renders as SVG
|
||||
const link = screen.getByRole('link')
|
||||
const svg = link.querySelector('svg')
|
||||
expect(svg).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render divider between credential selector and configuration button', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<Header {...props} />)
|
||||
|
||||
// Assert - Divider component should be rendered
|
||||
// Divider typically renders as a div with specific styling
|
||||
const divider = container.querySelector('[class*="divider"]') || container.querySelector('.mx-1.h-3\\.5')
|
||||
expect(divider).toBeInTheDocument()
|
||||
})
|
||||
it('should render credential selector', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByTestId('credential-selector')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Props Testing
|
||||
// ==========================================
|
||||
describe('Props', () => {
|
||||
describe('docTitle prop', () => {
|
||||
it('should display the document title', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docTitle: 'Getting Started Guide' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Getting Started Guide')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it.each([
|
||||
'Quick Start',
|
||||
'API Reference',
|
||||
'Configuration Guide',
|
||||
'Plugin Documentation',
|
||||
])('should display "%s" as document title', (title) => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docTitle: title })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(title)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('docLink prop', () => {
|
||||
it('should set correct href on documentation link', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docLink: 'https://custom.docs.com/guide' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveAttribute('href', 'https://custom.docs.com/guide')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'https://docs.dify.ai',
|
||||
'https://example.com/api',
|
||||
'/local/docs',
|
||||
])('should accept "%s" as docLink', (link) => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docLink: link })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', link)
|
||||
})
|
||||
})
|
||||
|
||||
describe('pluginName prop', () => {
|
||||
it('should pass pluginName to translation function', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ pluginName: 'MyPlugin' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - The translation mock returns the key with options
|
||||
// Tooltip uses the translated content
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('onClickConfiguration prop', () => {
|
||||
it('should call onClickConfiguration when configuration icon is clicked', () => {
|
||||
// Arrange
|
||||
const mockOnClick = vi.fn()
|
||||
const props = createDefaultProps({ onClickConfiguration: mockOnClick })
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Act - Find the configuration button and click the icon inside
|
||||
// The button contains the RiEqualizer2Line icon with onClick handler
|
||||
const configButton = screen.getByRole('button')
|
||||
const configIcon = configButton.querySelector('svg')
|
||||
expect(configIcon).toBeInTheDocument()
|
||||
fireEvent.click(configIcon!)
|
||||
|
||||
// Assert
|
||||
expect(mockOnClick).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not crash when onClickConfiguration is undefined', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ onClickConfiguration: undefined })
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Act - Find the configuration button and click the icon inside
|
||||
const configButton = screen.getByRole('button')
|
||||
const configIcon = configButton.querySelector('svg')
|
||||
expect(configIcon).toBeInTheDocument()
|
||||
fireEvent.click(configIcon!)
|
||||
|
||||
// Assert - Component should still be rendered (no crash)
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('CredentialSelector props passthrough', () => {
|
||||
it('should pass currentCredentialId to CredentialSelector', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-2' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - Should display the second credential
|
||||
expect(screen.getByText('Credential 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass credentials to CredentialSelector', () => {
|
||||
// Arrange
|
||||
const customCredentials = [
|
||||
createMockCredential({ id: 'custom-1', name: 'Custom Credential' }),
|
||||
]
|
||||
const props = createDefaultProps({
|
||||
credentials: customCredentials,
|
||||
currentCredentialId: 'custom-1',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Custom Credential')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass onCredentialChange to CredentialSelector', () => {
|
||||
// Arrange
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Act - Open dropdown and select a credential
|
||||
// Use getAllByTestId and select the first one (CredentialSelector's trigger)
|
||||
const triggers = screen.getAllByTestId('portal-trigger')
|
||||
fireEvent.click(triggers[0])
|
||||
const credential2 = screen.getByText('Credential 2')
|
||||
fireEvent.click(credential2)
|
||||
|
||||
// Assert
|
||||
expect(mockOnChange).toHaveBeenCalledWith('cred-2')
|
||||
})
|
||||
})
|
||||
it('should render configuration button', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByTestId('config-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// User Interactions
|
||||
// ==========================================
|
||||
describe('User Interactions', () => {
|
||||
it('should open external link in new tab when clicking documentation link', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - Link has target="_blank" for new tab
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
})
|
||||
|
||||
it('should allow credential selection through CredentialSelector', () => {
|
||||
// Arrange
|
||||
const mockOnChange = vi.fn()
|
||||
const props = createDefaultProps({ onCredentialChange: mockOnChange })
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Act - Open dropdown (use first trigger which is CredentialSelector's)
|
||||
const triggers = screen.getAllByTestId('portal-trigger')
|
||||
fireEvent.click(triggers[0])
|
||||
|
||||
// Assert - Dropdown should be open
|
||||
expect(screen.getByTestId('portal-content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should trigger configuration callback when clicking config icon', () => {
|
||||
// Arrange
|
||||
const mockOnConfig = vi.fn()
|
||||
const props = createDefaultProps({ onClickConfiguration: mockOnConfig })
|
||||
const { container } = render(<Header {...props} />)
|
||||
|
||||
// Act
|
||||
const configIcon = container.querySelector('.h-4.w-4')
|
||||
fireEvent.click(configIcon!)
|
||||
|
||||
// Assert
|
||||
expect(mockOnConfig).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Component Memoization
|
||||
// ==========================================
|
||||
describe('Component Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Assert
|
||||
expect(Header.$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
|
||||
it('should not re-render when props remain the same', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
const renderSpy = vi.fn()
|
||||
|
||||
const TrackedHeader: React.FC<HeaderProps> = (trackedProps) => {
|
||||
renderSpy()
|
||||
return <Header {...trackedProps} />
|
||||
}
|
||||
const MemoizedTracked = React.memo(TrackedHeader)
|
||||
|
||||
// Act
|
||||
const { rerender } = render(<MemoizedTracked {...props} />)
|
||||
rerender(<MemoizedTracked {...props} />)
|
||||
|
||||
// Assert - Should only render once due to same props
|
||||
expect(renderSpy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should re-render when docTitle changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docTitle: 'Original Title' })
|
||||
const { rerender } = render(<Header {...props} />)
|
||||
|
||||
// Assert initial
|
||||
expect(screen.getByText('Original Title')).toBeInTheDocument()
|
||||
|
||||
// Act
|
||||
rerender(<Header {...props} docTitle="Updated Title" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Updated Title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-render when currentCredentialId changes', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ currentCredentialId: 'cred-1' })
|
||||
const { rerender } = render(<Header {...props} />)
|
||||
|
||||
// Assert initial
|
||||
expect(screen.getByText('Credential 1')).toBeInTheDocument()
|
||||
|
||||
// Act
|
||||
rerender(<Header {...props} currentCredentialId="cred-2" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Credential 2')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Edge Cases
|
||||
// ==========================================
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty docTitle', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docTitle: '' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - Should render without crashing
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long docTitle', () => {
|
||||
// Arrange
|
||||
const longTitle = 'A'.repeat(200)
|
||||
const props = createDefaultProps({ docTitle: longTitle })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(longTitle)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in docTitle', () => {
|
||||
// Arrange
|
||||
const specialTitle = 'Docs & Guide <v2> "Special"'
|
||||
const props = createDefaultProps({ docTitle: specialTitle })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(specialTitle)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty credentials array', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({
|
||||
credentials: [],
|
||||
currentCredentialId: '',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - Should render without crashing
|
||||
expect(screen.getByRole('link')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in pluginName', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ pluginName: 'Plugin & Tool <v1>' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - Should render without crashing
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle unicode characters in docTitle', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docTitle: '文档说明 📚' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('文档说明 📚')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Styling
|
||||
// ==========================================
|
||||
describe('Styling', () => {
|
||||
it('should apply correct classes to container', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
const { container } = render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const rootDiv = container.firstChild as HTMLElement
|
||||
expect(rootDiv).toHaveClass('flex', 'items-center', 'justify-between', 'gap-x-2')
|
||||
})
|
||||
|
||||
it('should apply correct classes to documentation link', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveClass('system-xs-medium', 'text-text-accent')
|
||||
})
|
||||
|
||||
it('should apply shrink-0 to documentation link', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveClass('shrink-0')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Integration Tests
|
||||
// ==========================================
|
||||
describe('Integration', () => {
|
||||
it('should work with full credential workflow', () => {
|
||||
// Arrange
|
||||
const mockOnCredentialChange = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
onCredentialChange: mockOnCredentialChange,
|
||||
currentCredentialId: 'cred-1',
|
||||
})
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert initial state
|
||||
expect(screen.getByText('Credential 1')).toBeInTheDocument()
|
||||
|
||||
// Act - Open dropdown and select different credential
|
||||
// Use first trigger which is CredentialSelector's
|
||||
const triggers = screen.getAllByTestId('portal-trigger')
|
||||
fireEvent.click(triggers[0])
|
||||
|
||||
const credential3 = screen.getByText('Credential 3')
|
||||
fireEvent.click(credential3)
|
||||
|
||||
// Assert
|
||||
expect(mockOnCredentialChange).toHaveBeenCalledWith('cred-3')
|
||||
})
|
||||
|
||||
it('should display all components together correctly', () => {
|
||||
// Arrange
|
||||
const mockOnConfig = vi.fn()
|
||||
const props = createDefaultProps({
|
||||
docTitle: 'Integration Test Docs',
|
||||
docLink: 'https://test.com/docs',
|
||||
pluginName: 'TestPlugin',
|
||||
onClickConfiguration: mockOnConfig,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert - All main elements present
|
||||
expect(screen.getByText('Credential 1')).toBeInTheDocument() // CredentialSelector
|
||||
expect(screen.getByRole('button')).toBeInTheDocument() // Config button
|
||||
expect(screen.getByText('Integration Test Docs')).toBeInTheDocument() // Doc link
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', 'https://test.com/docs')
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================
|
||||
// Accessibility
|
||||
// ==========================================
|
||||
describe('Accessibility', () => {
|
||||
it('should have accessible link', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps({ docTitle: 'Accessible Docs' })
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link', { name: /Accessible Docs/i })
|
||||
expect(link).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have accessible button for configuration', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have noopener noreferrer for security on external links', () => {
|
||||
// Arrange
|
||||
const props = createDefaultProps()
|
||||
|
||||
// Act
|
||||
render(<Header {...props} />)
|
||||
|
||||
// Assert
|
||||
const link = screen.getByRole('link')
|
||||
expect(link).toHaveAttribute('rel', 'noopener noreferrer')
|
||||
})
|
||||
it('should link to external doc', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
const link = screen.getByText('Documentation').closest('a')
|
||||
expect(link).toHaveAttribute('href', 'https://docs.example.com')
|
||||
expect(link).toHaveAttribute('target', '_blank')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,38 +1,16 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import EmptyFolder from './empty-folder'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
describe('EmptyFolder', () => {
|
||||
it('should render without crashing', () => {
|
||||
it('should render empty folder message', () => {
|
||||
render(<EmptyFolder />)
|
||||
expect(screen.getByText('onlineDrive.emptyFolder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the empty folder text', () => {
|
||||
render(<EmptyFolder />)
|
||||
expect(screen.getByText('onlineDrive.emptyFolder')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have proper styling classes', () => {
|
||||
const { container } = render(<EmptyFolder />)
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper).toHaveClass('flex')
|
||||
expect(wrapper).toHaveClass('items-center')
|
||||
expect(wrapper).toHaveClass('justify-center')
|
||||
})
|
||||
|
||||
it('should be wrapped with React.memo', () => {
|
||||
expect((EmptyFolder as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Item from './item'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/checkbox', () => ({
|
||||
default: ({ checked, onCheck, disabled }: { checked: boolean, onCheck: () => void, disabled?: boolean }) => (
|
||||
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} disabled={disabled} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/radio/ui', () => ({
|
||||
default: ({ isChecked, onCheck }: { isChecked: boolean, onCheck: () => void }) => (
|
||||
<input type="radio" data-testid="radio" checked={isChecked} onChange={onCheck} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ children, popupContent }: { children: React.ReactNode, popupContent: string }) => (
|
||||
<div data-testid="tooltip" title={popupContent}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./file-icon', () => ({
|
||||
default: () => <span data-testid="file-icon" />,
|
||||
}))
|
||||
|
||||
describe('Item', () => {
|
||||
const makeFile = (type: string, name = 'test.pdf', size = 1024): OnlineDriveFile => ({
|
||||
id: 'f-1',
|
||||
name,
|
||||
type: type as OnlineDriveFile['type'],
|
||||
size,
|
||||
})
|
||||
|
||||
const defaultProps = {
|
||||
file: makeFile('file'),
|
||||
isSelected: false,
|
||||
onSelect: vi.fn(),
|
||||
onOpen: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render file name', () => {
|
||||
render(<Item {...defaultProps} />)
|
||||
expect(screen.getByText('test.pdf')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render checkbox for file type in multiple choice mode', () => {
|
||||
render(<Item {...defaultProps} />)
|
||||
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render radio for file type in single choice mode', () => {
|
||||
render(<Item {...defaultProps} isMultipleChoice={false} />)
|
||||
expect(screen.getByTestId('radio')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render checkbox for bucket type', () => {
|
||||
render(<Item {...defaultProps} file={makeFile('bucket', 'my-bucket')} />)
|
||||
expect(screen.queryByTestId('checkbox')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onOpen for folder click', () => {
|
||||
const file = makeFile('folder', 'my-folder')
|
||||
render(<Item {...defaultProps} file={file} />)
|
||||
fireEvent.click(screen.getByText('my-folder'))
|
||||
expect(defaultProps.onOpen).toHaveBeenCalledWith(file)
|
||||
})
|
||||
|
||||
it('should call onSelect for file click', () => {
|
||||
render(<Item {...defaultProps} />)
|
||||
fireEvent.click(screen.getByText('test.pdf'))
|
||||
expect(defaultProps.onSelect).toHaveBeenCalledWith(defaultProps.file)
|
||||
})
|
||||
|
||||
it('should not call handlers when disabled', () => {
|
||||
render(<Item {...defaultProps} disabled={true} />)
|
||||
fireEvent.click(screen.getByText('test.pdf'))
|
||||
expect(defaultProps.onSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render file icon', () => {
|
||||
render(<Item {...defaultProps} />)
|
||||
expect(screen.getByTestId('file-icon')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,50 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import CheckboxWithLabel from './checkbox-with-label'
|
||||
|
||||
vi.mock('@/app/components/base/checkbox', () => ({
|
||||
default: ({ checked, onCheck }: { checked: boolean, onCheck: () => void }) => (
|
||||
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ popupContent }: { popupContent: string }) => <div data-testid="tooltip">{popupContent}</div>,
|
||||
}))
|
||||
|
||||
describe('CheckboxWithLabel', () => {
|
||||
const defaultProps = {
|
||||
isChecked: false,
|
||||
onChange: vi.fn(),
|
||||
label: 'Test Label',
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render label text', () => {
|
||||
render(<CheckboxWithLabel {...defaultProps} />)
|
||||
expect(screen.getByText('Test Label')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render checkbox', () => {
|
||||
render(<CheckboxWithLabel {...defaultProps} />)
|
||||
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip when provided', () => {
|
||||
render(<CheckboxWithLabel {...defaultProps} tooltip="Help text" />)
|
||||
expect(screen.getByTestId('tooltip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render tooltip when not provided', () => {
|
||||
render(<CheckboxWithLabel {...defaultProps} />)
|
||||
expect(screen.queryByTestId('tooltip')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<CheckboxWithLabel {...defaultProps} className="custom-cls" />)
|
||||
expect(container.querySelector('.custom-cls')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,75 @@
|
||||
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import CrawledResultItem from './crawled-result-item'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<button data-testid="preview-button" onClick={onClick}>{children}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/checkbox', () => ({
|
||||
default: ({ checked, onCheck }: { checked: boolean, onCheck: () => void }) => (
|
||||
<input type="checkbox" data-testid="checkbox" checked={checked} onChange={onCheck} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/radio/ui', () => ({
|
||||
default: ({ isChecked, onCheck }: { isChecked: boolean, onCheck: () => void }) => (
|
||||
<input type="radio" data-testid="radio" checked={isChecked} onChange={onCheck} />
|
||||
),
|
||||
}))
|
||||
|
||||
describe('CrawledResultItem', () => {
|
||||
const defaultProps = {
|
||||
payload: {
|
||||
title: 'Test Page',
|
||||
source_url: 'https://example.com/page',
|
||||
markdown: '',
|
||||
description: '',
|
||||
} satisfies CrawlResultItemType,
|
||||
isChecked: false,
|
||||
onCheckChange: vi.fn(),
|
||||
isPreview: false,
|
||||
showPreview: true,
|
||||
onPreview: vi.fn(),
|
||||
isMultipleChoice: true,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render title and URL', () => {
|
||||
render(<CrawledResultItem {...defaultProps} />)
|
||||
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||
expect(screen.getByText('https://example.com/page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render checkbox in multiple choice mode', () => {
|
||||
render(<CrawledResultItem {...defaultProps} />)
|
||||
expect(screen.getByTestId('checkbox')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render radio in single choice mode', () => {
|
||||
render(<CrawledResultItem {...defaultProps} isMultipleChoice={false} />)
|
||||
expect(screen.getByTestId('radio')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show preview button when showPreview is true', () => {
|
||||
render(<CrawledResultItem {...defaultProps} />)
|
||||
expect(screen.getByTestId('preview-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show preview button when showPreview is false', () => {
|
||||
render(<CrawledResultItem {...defaultProps} showPreview={false} />)
|
||||
expect(screen.queryByTestId('preview-button')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,27 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import Crawling from './crawling'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('Crawling', () => {
|
||||
it('should render crawl progress', () => {
|
||||
render(<Crawling crawledNum={5} totalNum={10} />)
|
||||
expect(screen.getByText(/5/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/10/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render total page scraped label', () => {
|
||||
render(<Crawling crawledNum={0} totalNum={0} />)
|
||||
expect(screen.getByText(/stepOne\.website\.totalPageScraped/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<Crawling crawledNum={1} totalNum={5} className="custom" />)
|
||||
expect(container.querySelector('.custom')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,35 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import ErrorMessage from './error-message'
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiErrorWarningFill: () => <span data-testid="error-icon" />,
|
||||
}))
|
||||
|
||||
describe('ErrorMessage', () => {
|
||||
it('should render title', () => {
|
||||
render(<ErrorMessage title="Something went wrong" />)
|
||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render error icon', () => {
|
||||
render(<ErrorMessage title="Error" />)
|
||||
expect(screen.getByTestId('error-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render error message when provided', () => {
|
||||
render(<ErrorMessage title="Error" errorMsg="Detailed error info" />)
|
||||
expect(screen.getByText('Detailed error info')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render error message when not provided', () => {
|
||||
const { container } = render(<ErrorMessage title="Error" />)
|
||||
const textElements = container.querySelectorAll('.system-xs-regular')
|
||||
expect(textElements).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(<ErrorMessage title="Error" className="custom-cls" />)
|
||||
expect(container.querySelector('.custom-cls')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,320 +1,77 @@
|
||||
import type { CustomFile as File } from '@/models/datasets'
|
||||
import type { CustomFile } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import FilePreview from './file-preview'
|
||||
|
||||
// Uses global react-i18next mock from web/vitest.setup.ts
|
||||
|
||||
// Mock useFilePreview hook - needs to be mocked to control return values
|
||||
const mockUseFilePreview = vi.fn()
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useFilePreview: (fileID: string) => mockUseFilePreview(fileID),
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// 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: vi.fn() as () => Promise<ArrayBuffer>,
|
||||
bytes: vi.fn() as () => Promise<Uint8Array>,
|
||||
slice: vi.fn() as (start?: number, end?: number, contentType?: string) => Blob,
|
||||
stream: vi.fn() as () => ReadableStream<Uint8Array>,
|
||||
text: vi.fn() as () => Promise<string>,
|
||||
...overrides,
|
||||
} as File)
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiCloseLine: () => <span data-testid="close-icon" />,
|
||||
}))
|
||||
|
||||
const createMockFilePreviewData = (content: string = 'This is the file content') => ({
|
||||
content,
|
||||
})
|
||||
const mockFileData = { content: 'file content here with some text' }
|
||||
let mockIsFetching = false
|
||||
|
||||
const defaultProps = {
|
||||
file: createMockFile(),
|
||||
hidePreview: vi.fn(),
|
||||
}
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useFilePreview: () => ({
|
||||
data: mockIsFetching ? undefined : mockFileData,
|
||||
isFetching: mockIsFetching,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../../common/document-file-icon', () => ({
|
||||
default: () => <span data-testid="file-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('./loading', () => ({
|
||||
default: () => <div data-testid="loading" />,
|
||||
}))
|
||||
|
||||
describe('FilePreview', () => {
|
||||
const defaultProps = {
|
||||
file: {
|
||||
id: 'file-1',
|
||||
name: 'document.pdf',
|
||||
extension: 'pdf',
|
||||
size: 1024,
|
||||
} as CustomFile,
|
||||
hidePreview: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseFilePreview.mockReturnValue({
|
||||
data: undefined,
|
||||
isFetching: false,
|
||||
})
|
||||
mockIsFetching = 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')
|
||||
})
|
||||
it('should render preview label', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
expect(screen.getByText('addDocuments.stepOne.preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
it('should render file name', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
expect(screen.getByText('document.pdf')).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()
|
||||
})
|
||||
it('should render file content when loaded', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
expect(screen.getByText('file content here with some text')).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()
|
||||
})
|
||||
it('should render loading state', () => {
|
||||
mockIsFetching = true
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
expect(screen.getByTestId('loading')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = vi.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()
|
||||
})
|
||||
it('should call hidePreview when close button clicked', () => {
|
||||
render(<FilePreview {...defaultProps} />)
|
||||
const closeBtn = screen.getByTestId('close-icon').closest('button')!
|
||||
fireEvent.click(closeBtn)
|
||||
expect(defaultProps.hidePreview).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,256 +1,58 @@
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import WebsitePreview from './web-preview'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import WebPreview from './web-preview'
|
||||
|
||||
// Uses global react-i18next mock from web/vitest.setup.ts
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// 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,
|
||||
})
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiCloseLine: () => <span data-testid="close-icon" />,
|
||||
RiGlobalLine: () => <span data-testid="global-icon" />,
|
||||
}))
|
||||
|
||||
const defaultProps = {
|
||||
currentWebsite: createMockCrawlResult(),
|
||||
hidePreview: vi.fn(),
|
||||
}
|
||||
describe('WebPreview', () => {
|
||||
const defaultProps = {
|
||||
currentWebsite: {
|
||||
title: 'Test Page',
|
||||
source_url: 'https://example.com',
|
||||
markdown: 'Hello **markdown** content',
|
||||
description: '',
|
||||
} satisfies CrawlResultItem,
|
||||
hidePreview: vi.fn(),
|
||||
}
|
||||
|
||||
describe('WebsitePreview', () => {
|
||||
beforeEach(() => {
|
||||
vi.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()
|
||||
})
|
||||
it('should render preview label', () => {
|
||||
render(<WebPreview {...defaultProps} />)
|
||||
expect(screen.getByText('addDocuments.stepOne.preview')).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()
|
||||
})
|
||||
it('should render page title', () => {
|
||||
render(<WebPreview {...defaultProps} />)
|
||||
expect(screen.getByText('Test Page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
it('should call hidePreview when close button is clicked', () => {
|
||||
const hidePreview = vi.fn()
|
||||
|
||||
render(<WebsitePreview {...defaultProps} hidePreview={hidePreview} />)
|
||||
|
||||
const closeButton = screen.getByRole('button')
|
||||
fireEvent.click(closeButton)
|
||||
|
||||
expect(hidePreview).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
it('should render source URL', () => {
|
||||
render(<WebPreview {...defaultProps} />)
|
||||
expect(screen.getByText('https://example.com')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
it('should render markdown content', () => {
|
||||
render(<WebPreview {...defaultProps} />)
|
||||
expect(screen.getByText('Hello **markdown** content')).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 = vi.fn()
|
||||
const hidePreview2 = vi.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)
|
||||
})
|
||||
it('should call hidePreview when close button clicked', () => {
|
||||
render(<WebPreview {...defaultProps} />)
|
||||
const closeBtn = screen.getByTestId('close-icon').closest('button')!
|
||||
fireEvent.click(closeBtn)
|
||||
expect(defaultProps.hidePreview).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Header from './header'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiSearchEyeLine: () => <span data-testid="search-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, disabled, variant }: { children: React.ReactNode, onClick: () => void, disabled?: boolean, variant: string }) => (
|
||||
<button data-testid={`btn-${variant}`} onClick={onClick} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('Header', () => {
|
||||
const defaultProps = {
|
||||
onReset: vi.fn(),
|
||||
resetDisabled: false,
|
||||
previewDisabled: false,
|
||||
onPreview: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render chunk settings title', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByText('addDocuments.stepTwo.chunkSettings')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render reset and preview buttons', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
expect(screen.getByTestId('btn-ghost')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('btn-secondary-accent')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onReset when reset clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('btn-ghost'))
|
||||
expect(defaultProps.onReset).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onPreview when preview clicked', () => {
|
||||
render(<Header {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('btn-secondary-accent'))
|
||||
expect(defaultProps.onPreview).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable reset button when resetDisabled is true', () => {
|
||||
render(<Header {...defaultProps} resetDisabled={true} />)
|
||||
expect(screen.getByTestId('btn-ghost')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable preview button when previewDisabled is true', () => {
|
||||
render(<Header {...defaultProps} previewDisabled={true} />)
|
||||
expect(screen.getByTestId('btn-secondary-accent')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,112 @@
|
||||
import type { ChildChunkDetail, SegmentDetailModel } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import DrawerGroup from './drawer-group'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../common/full-screen-drawer', () => ({
|
||||
default: ({ isOpen, children }: { isOpen: boolean, children: React.ReactNode }) => (
|
||||
isOpen ? <div data-testid="full-screen-drawer">{children}</div> : null
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../segment-detail', () => ({
|
||||
default: () => <div data-testid="segment-detail" />,
|
||||
}))
|
||||
|
||||
vi.mock('../child-segment-detail', () => ({
|
||||
default: () => <div data-testid="child-segment-detail" />,
|
||||
}))
|
||||
|
||||
vi.mock('../new-child-segment', () => ({
|
||||
default: () => <div data-testid="new-child-segment" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/new-segment', () => ({
|
||||
default: () => <div data-testid="new-segment" />,
|
||||
}))
|
||||
|
||||
describe('DrawerGroup', () => {
|
||||
const defaultProps = {
|
||||
currSegment: { segInfo: undefined, showModal: false, isEditMode: false },
|
||||
onCloseSegmentDetail: vi.fn(),
|
||||
onUpdateSegment: vi.fn(),
|
||||
isRegenerationModalOpen: false,
|
||||
setIsRegenerationModalOpen: vi.fn(),
|
||||
showNewSegmentModal: false,
|
||||
onCloseNewSegmentModal: vi.fn(),
|
||||
onSaveNewSegment: vi.fn(),
|
||||
viewNewlyAddedChunk: vi.fn(),
|
||||
currChildChunk: { childChunkInfo: undefined, showModal: false },
|
||||
currChunkId: 'chunk-1',
|
||||
onCloseChildSegmentDetail: vi.fn(),
|
||||
onUpdateChildChunk: vi.fn(),
|
||||
showNewChildSegmentModal: false,
|
||||
onCloseNewChildChunkModal: vi.fn(),
|
||||
onSaveNewChildChunk: vi.fn(),
|
||||
viewNewlyAddedChildChunk: vi.fn(),
|
||||
fullScreen: false,
|
||||
docForm: ChunkingMode.text,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render nothing when all modals are closed', () => {
|
||||
const { container } = render(<DrawerGroup {...defaultProps} />)
|
||||
expect(container.querySelector('[data-testid="full-screen-drawer"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('should render segment detail when segment modal is open', () => {
|
||||
render(
|
||||
<DrawerGroup
|
||||
{...defaultProps}
|
||||
currSegment={{ segInfo: { id: 'seg-1' } as SegmentDetailModel, showModal: true, isEditMode: true }}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('segment-detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render new segment modal when showNewSegmentModal is true', () => {
|
||||
render(
|
||||
<DrawerGroup {...defaultProps} showNewSegmentModal={true} />,
|
||||
)
|
||||
expect(screen.getByTestId('new-segment')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render child segment detail when child chunk modal is open', () => {
|
||||
render(
|
||||
<DrawerGroup
|
||||
{...defaultProps}
|
||||
currChildChunk={{ childChunkInfo: { id: 'child-1' } as ChildChunkDetail, showModal: true }}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('child-segment-detail')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render new child segment modal when showNewChildSegmentModal is true', () => {
|
||||
render(
|
||||
<DrawerGroup {...defaultProps} showNewChildSegmentModal={true} />,
|
||||
)
|
||||
expect(screen.getByTestId('new-child-segment')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render multiple drawers simultaneously', () => {
|
||||
render(
|
||||
<DrawerGroup
|
||||
{...defaultProps}
|
||||
currSegment={{ segInfo: { id: 'seg-1' } as SegmentDetailModel, showModal: true }}
|
||||
showNewChildSegmentModal={true}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByTestId('segment-detail')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('new-child-segment')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,80 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import MenuBar from './menu-bar'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiSearchLine: () => <span data-testid="search-icon" />,
|
||||
RiCloseLine: () => <span data-testid="close-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('../display-toggle', () => ({
|
||||
default: ({ isCollapsed, toggleCollapsed }: { isCollapsed: boolean, toggleCollapsed: () => void }) => (
|
||||
<button data-testid="display-toggle" onClick={toggleCollapsed}>
|
||||
{isCollapsed ? 'collapsed' : 'expanded'}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../status-item', () => ({
|
||||
default: ({ item }: { item: { name: string } }) => <div data-testid="status-item">{item.name}</div>,
|
||||
}))
|
||||
|
||||
describe('MenuBar', () => {
|
||||
const defaultProps = {
|
||||
isAllSelected: false,
|
||||
isSomeSelected: false,
|
||||
onSelectedAll: vi.fn(),
|
||||
isLoading: false,
|
||||
totalText: '10 Chunks',
|
||||
statusList: [
|
||||
{ value: 'all', name: 'All' },
|
||||
{ value: 0, name: 'Enabled' },
|
||||
{ value: 1, name: 'Disabled' },
|
||||
],
|
||||
selectDefaultValue: 'all' as const,
|
||||
onChangeStatus: vi.fn(),
|
||||
inputValue: '',
|
||||
onInputChange: vi.fn(),
|
||||
isCollapsed: false,
|
||||
toggleCollapsed: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render total text', () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
expect(screen.getByText('10 Chunks')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render checkbox', () => {
|
||||
const { container } = render(<MenuBar {...defaultProps} />)
|
||||
const checkbox = container.querySelector('[class*="shrink-0"]')
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onInputChange when input changes', () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
const input = screen.getByRole('textbox')
|
||||
fireEvent.change(input, { target: { value: 'test search' } })
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith('test search')
|
||||
})
|
||||
|
||||
it('should render display toggle', () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
expect(screen.getByTestId('display-toggle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call toggleCollapsed when display toggle clicked', () => {
|
||||
render(<MenuBar {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('display-toggle'))
|
||||
expect(defaultProps.toggleCollapsed).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,102 @@
|
||||
import type { SegmentDetailModel } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FullDocModeContent, GeneralModeContent } from './segment-list-content'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../child-segment-list', () => ({
|
||||
default: ({ parentChunkId }: { parentChunkId: string }) => (
|
||||
<div data-testid="child-segment-list">{parentChunkId}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../segment-card', () => ({
|
||||
default: ({ detail }: { detail: { id: string } }) => (
|
||||
<div data-testid="segment-card">{detail?.id}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../segment-list', () => {
|
||||
const SegmentList = vi.fn(({ items }: { items: { id: string }[] }) => (
|
||||
<div data-testid="segment-list">
|
||||
{items?.length ?? 0}
|
||||
{' '}
|
||||
items
|
||||
</div>
|
||||
))
|
||||
return { default: SegmentList }
|
||||
})
|
||||
|
||||
describe('FullDocModeContent', () => {
|
||||
const defaultProps = {
|
||||
segments: [{ id: 'seg-1', position: 1, content: 'test', word_count: 10 }] as SegmentDetailModel[],
|
||||
childSegments: [],
|
||||
isLoadingSegmentList: false,
|
||||
isLoadingChildSegmentList: false,
|
||||
currSegmentId: undefined,
|
||||
onClickCard: vi.fn(),
|
||||
onDeleteChildChunk: vi.fn(),
|
||||
handleInputChange: vi.fn(),
|
||||
handleAddNewChildChunk: vi.fn(),
|
||||
onClickSlice: vi.fn(),
|
||||
archived: false,
|
||||
childChunkTotal: 0,
|
||||
inputValue: '',
|
||||
onClearFilter: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render segment card with first segment', () => {
|
||||
render(<FullDocModeContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('segment-card')).toHaveTextContent('seg-1')
|
||||
})
|
||||
|
||||
it('should render child segment list', () => {
|
||||
render(<FullDocModeContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('child-segment-list')).toHaveTextContent('seg-1')
|
||||
})
|
||||
|
||||
it('should apply overflow-y-hidden when loading', () => {
|
||||
const { container } = render(
|
||||
<FullDocModeContent {...defaultProps} isLoadingSegmentList={true} />,
|
||||
)
|
||||
expect(container.firstChild).toHaveClass('overflow-y-hidden')
|
||||
})
|
||||
|
||||
it('should apply overflow-y-auto when not loading', () => {
|
||||
const { container } = render(<FullDocModeContent {...defaultProps} />)
|
||||
expect(container.firstChild).toHaveClass('overflow-y-auto')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GeneralModeContent', () => {
|
||||
const defaultProps = {
|
||||
segmentListRef: { current: null },
|
||||
embeddingAvailable: true,
|
||||
isLoadingSegmentList: false,
|
||||
segments: [{ id: 'seg-1' }, { id: 'seg-2' }] as SegmentDetailModel[],
|
||||
selectedSegmentIds: [],
|
||||
onSelected: vi.fn(),
|
||||
onChangeSwitch: vi.fn(),
|
||||
onDelete: vi.fn(),
|
||||
onClickCard: vi.fn(),
|
||||
archived: false,
|
||||
onDeleteChildChunk: vi.fn(),
|
||||
handleAddNewChildChunk: vi.fn(),
|
||||
onClickSlice: vi.fn(),
|
||||
onClearFilter: vi.fn(),
|
||||
}
|
||||
|
||||
it('should render segment list with items', () => {
|
||||
render(<GeneralModeContent {...defaultProps} />)
|
||||
expect(screen.getByTestId('segment-list')).toHaveTextContent('2 items')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,77 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { EditSlice } from './edit-slice'
|
||||
|
||||
vi.mock('@floating-ui/react', () => ({
|
||||
autoUpdate: vi.fn(),
|
||||
flip: vi.fn(),
|
||||
shift: vi.fn(),
|
||||
offset: vi.fn(),
|
||||
FloatingFocusManager: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
useFloating: () => ({
|
||||
refs: { setReference: vi.fn(), setFloating: vi.fn() },
|
||||
floatingStyles: {},
|
||||
context: { open: false, onOpenChange: vi.fn(), refs: { domReference: { current: null } }, nodeId: undefined },
|
||||
}),
|
||||
useHover: () => ({}),
|
||||
useDismiss: () => ({}),
|
||||
useRole: () => ({}),
|
||||
useInteractions: () => ({
|
||||
getReferenceProps: () => ({}),
|
||||
getFloatingProps: () => ({}),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiDeleteBinLine: () => <span data-testid="delete-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/action-button', () => {
|
||||
const comp = ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<button data-testid="action-button" onClick={onClick}>{children}</button>
|
||||
)
|
||||
return {
|
||||
default: comp,
|
||||
ActionButtonState: { Destructive: 'destructive' },
|
||||
}
|
||||
})
|
||||
|
||||
describe('EditSlice', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render label and text', () => {
|
||||
render(<EditSlice label="C-1" text="chunk content" onDelete={vi.fn()} />)
|
||||
expect(screen.getByText('C-1')).toBeInTheDocument()
|
||||
expect(screen.getByText('chunk content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render divider by default', () => {
|
||||
const { container } = render(<EditSlice label="C-1" text="text" onDelete={vi.fn()} />)
|
||||
// SliceDivider renders a zero-width space
|
||||
const spans = container.querySelectorAll('span')
|
||||
const dividerSpan = Array.from(spans).find(s => s.textContent?.includes('\u200B'))
|
||||
expect(dividerSpan).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should not render divider when showDivider is false', () => {
|
||||
const { container } = render(
|
||||
<EditSlice label="C-1" text="text" onDelete={vi.fn()} showDivider={false} />,
|
||||
)
|
||||
const spans = container.querySelectorAll('span')
|
||||
const dividerSpan = Array.from(spans).find(s => s.textContent === '\u200B')
|
||||
expect(dividerSpan).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should apply custom labelClassName', () => {
|
||||
render(<EditSlice label="C-1" text="text" onDelete={vi.fn()} labelClassName="custom-label" />)
|
||||
const labelParent = screen.getByText('C-1').parentElement!
|
||||
expect(labelParent).toHaveClass('custom-label')
|
||||
})
|
||||
|
||||
it('should apply custom contentClassName', () => {
|
||||
render(<EditSlice label="C-1" text="content" onDelete={vi.fn()} contentClassName="custom-content" />)
|
||||
expect(screen.getByText('content')).toHaveClass('custom-content')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,57 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PreviewSlice } from './preview-slice'
|
||||
|
||||
vi.mock('@floating-ui/react', () => ({
|
||||
autoUpdate: vi.fn(),
|
||||
flip: vi.fn(),
|
||||
shift: vi.fn(),
|
||||
inline: vi.fn(),
|
||||
useFloating: () => ({
|
||||
refs: { setReference: vi.fn(), setFloating: vi.fn() },
|
||||
floatingStyles: {},
|
||||
context: { open: false, onOpenChange: vi.fn(), refs: { domReference: { current: null } }, nodeId: undefined },
|
||||
}),
|
||||
useHover: () => ({}),
|
||||
useDismiss: () => ({}),
|
||||
useRole: () => ({}),
|
||||
useInteractions: () => ({
|
||||
getReferenceProps: () => ({}),
|
||||
getFloatingProps: () => ({}),
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('PreviewSlice', () => {
|
||||
it('should render label and text', () => {
|
||||
render(<PreviewSlice label="P-1" text="preview content" tooltip="tooltip text" />)
|
||||
expect(screen.getByText('P-1')).toBeInTheDocument()
|
||||
expect(screen.getByText('preview content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show tooltip by default', () => {
|
||||
render(<PreviewSlice label="P-1" text="text" tooltip="tooltip" />)
|
||||
expect(screen.queryByText('tooltip')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply custom className', () => {
|
||||
const { container } = render(
|
||||
<PreviewSlice label="P-1" text="text" tooltip="tip" className="custom-class" />,
|
||||
)
|
||||
const sliceContainer = container.querySelector('.custom-class')
|
||||
expect(sliceContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply labelInnerClassName', () => {
|
||||
render(<PreviewSlice label="Label" text="text" tooltip="tip" labelInnerClassName="inner-cls" />)
|
||||
expect(screen.getByText('Label')).toHaveClass('inner-cls')
|
||||
})
|
||||
|
||||
it('should render divider', () => {
|
||||
const { container } = render(
|
||||
<PreviewSlice label="P-1" text="text" tooltip="tip" />,
|
||||
)
|
||||
const spans = container.querySelectorAll('span')
|
||||
const dividerSpan = Array.from(spans).find(s => s.textContent?.includes('\u200B'))
|
||||
expect(dividerSpan).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,85 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { SliceContainer, SliceContent, SliceDivider, SliceLabel } from './shared'
|
||||
|
||||
describe('SliceContainer', () => {
|
||||
it('should render children', () => {
|
||||
render(<SliceContainer>content</SliceContainer>)
|
||||
expect(screen.getByText('content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should be a span element', () => {
|
||||
render(<SliceContainer>text</SliceContainer>)
|
||||
expect(screen.getByText('text').tagName).toBe('SPAN')
|
||||
})
|
||||
|
||||
it('should merge custom className', () => {
|
||||
render(<SliceContainer className="custom">text</SliceContainer>)
|
||||
expect(screen.getByText('text')).toHaveClass('custom')
|
||||
})
|
||||
|
||||
it('should have display name', () => {
|
||||
expect(SliceContainer.displayName).toBe('SliceContainer')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SliceLabel', () => {
|
||||
it('should render children with uppercase text', () => {
|
||||
render(<SliceLabel>Label</SliceLabel>)
|
||||
expect(screen.getByText('Label')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply label styling', () => {
|
||||
render(<SliceLabel>Label</SliceLabel>)
|
||||
const outer = screen.getByText('Label').parentElement!
|
||||
expect(outer).toHaveClass('uppercase')
|
||||
})
|
||||
|
||||
it('should apply labelInnerClassName to inner span', () => {
|
||||
render(<SliceLabel labelInnerClassName="inner-class">Label</SliceLabel>)
|
||||
expect(screen.getByText('Label')).toHaveClass('inner-class')
|
||||
})
|
||||
|
||||
it('should have display name', () => {
|
||||
expect(SliceLabel.displayName).toBe('SliceLabel')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SliceContent', () => {
|
||||
it('should render children', () => {
|
||||
render(<SliceContent>Content</SliceContent>)
|
||||
expect(screen.getByText('Content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply whitespace-pre-line and break-all', () => {
|
||||
render(<SliceContent>Content</SliceContent>)
|
||||
const el = screen.getByText('Content')
|
||||
expect(el).toHaveClass('whitespace-pre-line')
|
||||
expect(el).toHaveClass('break-all')
|
||||
})
|
||||
|
||||
it('should have display name', () => {
|
||||
expect(SliceContent.displayName).toBe('SliceContent')
|
||||
})
|
||||
})
|
||||
|
||||
describe('SliceDivider', () => {
|
||||
it('should render as span', () => {
|
||||
const { container } = render(<SliceDivider />)
|
||||
expect(container.querySelector('span')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should contain zero-width space', () => {
|
||||
const { container } = render(<SliceDivider />)
|
||||
expect(container.textContent).toContain('\u200B')
|
||||
})
|
||||
|
||||
it('should merge custom className', () => {
|
||||
const { container } = render(<SliceDivider className="custom" />)
|
||||
expect(container.querySelector('span')).toHaveClass('custom')
|
||||
})
|
||||
|
||||
it('should have display name', () => {
|
||||
expect(SliceDivider.displayName).toBe('SliceDivider')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,27 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { FormattedText } from './formatted'
|
||||
|
||||
describe('FormattedText', () => {
|
||||
it('should render children', () => {
|
||||
render(<FormattedText>Hello World</FormattedText>)
|
||||
expect(screen.getByText('Hello World')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should apply leading-7 class by default', () => {
|
||||
render(<FormattedText>Text</FormattedText>)
|
||||
expect(screen.getByText('Text')).toHaveClass('leading-7')
|
||||
})
|
||||
|
||||
it('should merge custom className', () => {
|
||||
render(<FormattedText className="custom-class">Text</FormattedText>)
|
||||
const el = screen.getByText('Text')
|
||||
expect(el).toHaveClass('leading-7')
|
||||
expect(el).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should render as a p element', () => {
|
||||
render(<FormattedText>Text</FormattedText>)
|
||||
expect(screen.getByText('Text').tagName).toBe('P')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,143 @@
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ChunkDetailModal from './chunk-detail-modal'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: { num?: number }) => opts?.num ? `${key}-${opts.num}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/file-uploader/file-type-icon', () => ({
|
||||
default: () => <span data-testid="file-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/markdown', () => ({
|
||||
Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/modal', () => ({
|
||||
default: ({ children, title, onClose }: { children: React.ReactNode, title: string, onClose: () => void }) => (
|
||||
<div data-testid="modal">
|
||||
<div data-testid="modal-title">{title}</div>
|
||||
<button data-testid="modal-close" onClick={onClose}>close</button>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../../common/image-list', () => ({
|
||||
default: () => <div data-testid="image-list" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../documents/detail/completed/common/dot', () => ({
|
||||
default: () => <span data-testid="dot" />,
|
||||
}))
|
||||
|
||||
vi.mock('../../documents/detail/completed/common/segment-index-tag', () => ({
|
||||
SegmentIndexTag: ({ positionId }: { positionId: number }) => <span data-testid="segment-index-tag">{positionId}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../../documents/detail/completed/common/summary-text', () => ({
|
||||
default: ({ value }: { value: string }) => <div data-testid="summary-text">{value}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/completed/common/tag', () => ({
|
||||
default: ({ text }: { text: string }) => <span data-testid="tag">{text}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('./child-chunks-item', () => ({
|
||||
default: ({ payload }: { payload: { id: string } }) => <div data-testid="child-chunk">{payload.id}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./mask', () => ({
|
||||
default: () => <div data-testid="mask" />,
|
||||
}))
|
||||
|
||||
vi.mock('./score', () => ({
|
||||
default: ({ value }: { value: number }) => <span data-testid="score">{value}</span>,
|
||||
}))
|
||||
|
||||
const makePayload = (overrides: Record<string, unknown> = {}): HitTesting => {
|
||||
const segmentOverrides = (overrides.segment ?? {}) as Record<string, unknown>
|
||||
const segment = {
|
||||
position: 1,
|
||||
content: 'chunk content',
|
||||
sign_content: '',
|
||||
keywords: [],
|
||||
document: { name: 'file.pdf' },
|
||||
answer: '',
|
||||
word_count: 100,
|
||||
...segmentOverrides,
|
||||
}
|
||||
return {
|
||||
segment,
|
||||
content: segment,
|
||||
score: 0.85,
|
||||
tsne_position: { x: 0, y: 0 },
|
||||
child_chunks: (overrides.child_chunks ?? []) as HitTesting['child_chunks'],
|
||||
files: (overrides.files ?? []) as HitTesting['files'],
|
||||
summary: (overrides.summary ?? '') as string,
|
||||
} as unknown as HitTesting
|
||||
}
|
||||
|
||||
describe('ChunkDetailModal', () => {
|
||||
const onHide = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render modal with title', () => {
|
||||
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
|
||||
expect(screen.getByTestId('modal-title')).toHaveTextContent('chunkDetail')
|
||||
})
|
||||
|
||||
it('should render segment index tag and score', () => {
|
||||
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
|
||||
expect(screen.getByTestId('segment-index-tag')).toHaveTextContent('1')
|
||||
expect(screen.getByTestId('score')).toHaveTextContent('0.85')
|
||||
})
|
||||
|
||||
it('should render markdown content', () => {
|
||||
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
|
||||
expect(screen.getByTestId('markdown')).toHaveTextContent('chunk content')
|
||||
})
|
||||
|
||||
it('should render QA content when answer exists', () => {
|
||||
const payload = makePayload({
|
||||
segment: { answer: 'answer text', content: 'question text' },
|
||||
})
|
||||
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
|
||||
expect(screen.getByText('question text')).toBeInTheDocument()
|
||||
expect(screen.getByText('answer text')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render keywords when present and not parent-child', () => {
|
||||
const payload = makePayload({
|
||||
segment: { keywords: ['k1', 'k2'] },
|
||||
})
|
||||
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
|
||||
expect(screen.getAllByTestId('tag')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render child chunks section for parent-child retrieval', () => {
|
||||
const payload = makePayload({
|
||||
child_chunks: [{ id: 'c1' }, { id: 'c2' }],
|
||||
})
|
||||
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
|
||||
expect(screen.getAllByTestId('child-chunk')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render summary text when summary exists', () => {
|
||||
const payload = makePayload({ summary: 'test summary' })
|
||||
render(<ChunkDetailModal payload={payload} onHide={onHide} />)
|
||||
expect(screen.getByTestId('summary-text')).toHaveTextContent('test summary')
|
||||
})
|
||||
|
||||
it('should render mask overlay', () => {
|
||||
render(<ChunkDetailModal payload={makePayload()} onHide={onHide} />)
|
||||
expect(screen.getByTestId('mask')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,112 @@
|
||||
import type { Query } from '@/models/datasets'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import QueryInput from './index'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: (props: Record<string, unknown>) => <div data-testid="next-image" role="img" aria-label={props.alt as string} />,
|
||||
}))
|
||||
|
||||
vi.mock('uuid', () => ({
|
||||
v4: () => 'mock-uuid',
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiEqualizer2Line: () => <span data-testid="equalizer-icon" />,
|
||||
RiPlayCircleLine: () => <span data-testid="play-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, disabled, loading }: { children: React.ReactNode, onClick: () => void, disabled?: boolean, loading?: boolean }) => (
|
||||
<button data-testid="submit-button" onClick={onClick} disabled={disabled || loading}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/image-uploader/image-uploader-in-retrieval-testing', () => ({
|
||||
default: ({ textArea, actionButton }: { textArea: React.ReactNode, actionButton: React.ReactNode }) => (
|
||||
<div data-testid="image-uploader">
|
||||
{textArea}
|
||||
{actionButton}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/retrieval-method-info', () => ({
|
||||
getIcon: () => '/test-icon.png',
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/hit-testing/modify-external-retrieval-modal', () => ({
|
||||
default: () => <div data-testid="external-retrieval-modal" />,
|
||||
}))
|
||||
|
||||
vi.mock('./textarea', () => ({
|
||||
default: ({ text }: { text: string }) => <textarea data-testid="textarea" defaultValue={text} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: () => false,
|
||||
}))
|
||||
|
||||
describe('QueryInput', () => {
|
||||
const defaultProps = {
|
||||
onUpdateList: vi.fn(),
|
||||
setHitResult: vi.fn(),
|
||||
setExternalHitResult: vi.fn(),
|
||||
loading: false,
|
||||
queries: [{ content: 'test query', content_type: 'text_query', file_info: null }] satisfies Query[],
|
||||
setQueries: vi.fn(),
|
||||
isExternal: false,
|
||||
onClickRetrievalMethod: vi.fn(),
|
||||
retrievalConfig: { search_method: 'semantic_search' } as RetrievalConfig,
|
||||
isEconomy: false,
|
||||
hitTestingMutation: vi.fn(),
|
||||
externalKnowledgeBaseHitTestingMutation: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render title', () => {
|
||||
render(<QueryInput {...defaultProps} />)
|
||||
expect(screen.getByText('input.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render textarea with query text', () => {
|
||||
render(<QueryInput {...defaultProps} />)
|
||||
expect(screen.getByTestId('textarea')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render submit button', () => {
|
||||
render(<QueryInput {...defaultProps} />)
|
||||
expect(screen.getByTestId('submit-button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should disable submit button when text is empty', () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
queries: [{ content: '', content_type: 'text_query', file_info: null }] satisfies Query[],
|
||||
}
|
||||
render(<QueryInput {...props} />)
|
||||
expect(screen.getByTestId('submit-button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should render retrieval method for non-external mode', () => {
|
||||
render(<QueryInput {...defaultProps} />)
|
||||
expect(screen.getByText('retrieval.semantic_search.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render settings button for external mode', () => {
|
||||
render(<QueryInput {...defaultProps} isExternal={true} />)
|
||||
expect(screen.getByText('settingTitle')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,120 @@
|
||||
import type { HitTestingRecord } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import Records from './records'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-timestamp', () => ({
|
||||
default: () => ({
|
||||
formatTime: (ts: number, _fmt: string) => `time-${ts}`,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiApps2Line: (props: Record<string, unknown>) => <span data-testid="apps-icon" {...props} />,
|
||||
RiFocus2Line: (props: Record<string, unknown>) => <span data-testid="focus-icon" {...props} />,
|
||||
RiArrowDownLine: (props: Record<string, unknown>) => <span data-testid="arrow-down" {...props} />,
|
||||
}))
|
||||
|
||||
vi.mock('../../common/image-list', () => ({
|
||||
default: () => <div data-testid="image-list" />,
|
||||
}))
|
||||
|
||||
const makeRecord = (id: string, source: string, created_at: number, content = 'query text') => ({
|
||||
id,
|
||||
source,
|
||||
created_at,
|
||||
queries: [{ content, content_type: 'text_query', file_info: null }],
|
||||
}) as unknown as HitTestingRecord
|
||||
|
||||
describe('Records', () => {
|
||||
const mockOnClick = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render table headers', () => {
|
||||
render(<Records records={[]} onClickRecord={mockOnClick} />)
|
||||
expect(screen.getByText('table.header.queryContent')).toBeInTheDocument()
|
||||
expect(screen.getByText('table.header.source')).toBeInTheDocument()
|
||||
expect(screen.getByText('table.header.time')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render records', () => {
|
||||
const records = [
|
||||
makeRecord('1', 'app', 1000),
|
||||
makeRecord('2', 'hit_testing', 2000),
|
||||
]
|
||||
render(<Records records={records} onClickRecord={mockOnClick} />)
|
||||
expect(screen.getAllByText('query text')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should show app icon for app source', () => {
|
||||
const records = [makeRecord('1', 'app', 1000)]
|
||||
render(<Records records={records} onClickRecord={mockOnClick} />)
|
||||
expect(screen.getByTestId('apps-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show focus icon for non-app source', () => {
|
||||
const records = [makeRecord('1', 'hit_testing', 1000)]
|
||||
render(<Records records={records} onClickRecord={mockOnClick} />)
|
||||
expect(screen.getByTestId('focus-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onClickRecord when row clicked', () => {
|
||||
const records = [makeRecord('1', 'app', 1000)]
|
||||
render(<Records records={records} onClickRecord={mockOnClick} />)
|
||||
fireEvent.click(screen.getByText('query text'))
|
||||
expect(mockOnClick).toHaveBeenCalledWith(records[0])
|
||||
})
|
||||
|
||||
it('should sort records by time descending by default', () => {
|
||||
const records = [
|
||||
makeRecord('1', 'app', 1000, 'early'),
|
||||
makeRecord('2', 'app', 3000, 'late'),
|
||||
makeRecord('3', 'app', 2000, 'mid'),
|
||||
]
|
||||
render(<Records records={records} onClickRecord={mockOnClick} />)
|
||||
const rows = screen.getAllByRole('row').slice(1) // skip header
|
||||
expect(rows[0]).toHaveTextContent('late')
|
||||
expect(rows[1]).toHaveTextContent('mid')
|
||||
expect(rows[2]).toHaveTextContent('early')
|
||||
})
|
||||
|
||||
it('should toggle sort order on time header click', () => {
|
||||
const records = [
|
||||
makeRecord('1', 'app', 1000, 'early'),
|
||||
makeRecord('2', 'app', 3000, 'late'),
|
||||
]
|
||||
render(<Records records={records} onClickRecord={mockOnClick} />)
|
||||
|
||||
// Default: desc, so late first
|
||||
let rows = screen.getAllByRole('row').slice(1)
|
||||
expect(rows[0]).toHaveTextContent('late')
|
||||
|
||||
// Click to sort asc
|
||||
fireEvent.click(screen.getByText('table.header.time'))
|
||||
rows = screen.getAllByRole('row').slice(1)
|
||||
expect(rows[0]).toHaveTextContent('early')
|
||||
})
|
||||
|
||||
it('should render image list for image queries', () => {
|
||||
const records = [{
|
||||
id: '1',
|
||||
source: 'app',
|
||||
created_at: 1000,
|
||||
queries: [
|
||||
{ content: '', content_type: 'text_query', file_info: null },
|
||||
{ content: '', content_type: 'image_query', file_info: { name: 'img.png', mime_type: 'image/png', source_url: 'url', size: 100, extension: 'png' } },
|
||||
],
|
||||
}] as unknown as HitTestingRecord[]
|
||||
render(<Records records={records} onClickRecord={mockOnClick} />)
|
||||
expect(screen.getByTestId('image-list')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,134 @@
|
||||
import type { HitTesting } from '@/models/datasets'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ResultItem from './result-item'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: { num?: number }) => opts?.num ? `${key}-${opts.num}` : key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => {
|
||||
// eslint-disable-next-line ts/no-require-imports
|
||||
const { useState } = require('react')
|
||||
return {
|
||||
useBoolean: (initial: boolean) => {
|
||||
const [val, setVal] = useState(initial)
|
||||
return [val, { toggle: () => setVal((v: boolean) => !v), setTrue: () => setVal(true), setFalse: () => setVal(false) }]
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiArrowDownSLine: () => <span data-testid="arrow-down" />,
|
||||
RiArrowRightSLine: () => <span data-testid="arrow-right" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/markdown', () => ({
|
||||
Markdown: ({ content }: { content: string }) => <div data-testid="markdown">{content}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('../../common/image-list', () => ({
|
||||
default: () => <div data-testid="image-list" />,
|
||||
}))
|
||||
|
||||
vi.mock('./child-chunks-item', () => ({
|
||||
default: ({ payload }: { payload: { id: string } }) => <div data-testid="child-chunk">{payload.id}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./chunk-detail-modal', () => ({
|
||||
default: () => <div data-testid="chunk-detail-modal" />,
|
||||
}))
|
||||
|
||||
vi.mock('./result-item-footer', () => ({
|
||||
default: ({ docTitle }: { docTitle: string }) => <div data-testid="result-item-footer">{docTitle}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./result-item-meta', () => ({
|
||||
default: ({ positionId }: { positionId: number }) => <div data-testid="result-item-meta">{positionId}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/completed/common/summary-label', () => ({
|
||||
default: ({ summary }: { summary: string }) => <div data-testid="summary-label">{summary}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/completed/common/tag', () => ({
|
||||
default: ({ text }: { text: string }) => <span data-testid="tag">{text}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/hit-testing/utils/extension-to-file-type', () => ({
|
||||
extensionToFileType: () => 'pdf',
|
||||
}))
|
||||
|
||||
const makePayload = (overrides: Record<string, unknown> = {}): HitTesting => {
|
||||
const segmentOverrides = (overrides.segment ?? {}) as Record<string, unknown>
|
||||
const segment = {
|
||||
position: 1,
|
||||
word_count: 100,
|
||||
content: 'test content',
|
||||
sign_content: '',
|
||||
keywords: [],
|
||||
document: { name: 'file.pdf' },
|
||||
answer: '',
|
||||
...segmentOverrides,
|
||||
}
|
||||
return {
|
||||
segment,
|
||||
content: segment,
|
||||
score: 0.95,
|
||||
tsne_position: { x: 0, y: 0 },
|
||||
child_chunks: (overrides.child_chunks ?? []) as HitTesting['child_chunks'],
|
||||
files: (overrides.files ?? []) as HitTesting['files'],
|
||||
summary: (overrides.summary ?? '') as string,
|
||||
} as unknown as HitTesting
|
||||
}
|
||||
|
||||
describe('ResultItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render meta, content, and footer', () => {
|
||||
render(<ResultItem payload={makePayload()} />)
|
||||
expect(screen.getByTestId('result-item-meta')).toHaveTextContent('1')
|
||||
expect(screen.getByTestId('markdown')).toHaveTextContent('test content')
|
||||
expect(screen.getByTestId('result-item-footer')).toHaveTextContent('file.pdf')
|
||||
})
|
||||
|
||||
it('should render keywords when no child_chunks', () => {
|
||||
const payload = makePayload({
|
||||
segment: { keywords: ['key1', 'key2'] },
|
||||
})
|
||||
render(<ResultItem payload={payload} />)
|
||||
expect(screen.getAllByTestId('tag')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render child chunks when present', () => {
|
||||
const payload = makePayload({
|
||||
child_chunks: [{ id: 'c1' }, { id: 'c2' }],
|
||||
})
|
||||
render(<ResultItem payload={payload} />)
|
||||
expect(screen.getAllByTestId('child-chunk')).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should render summary label when summary exists', () => {
|
||||
const payload = makePayload({ summary: 'test summary' })
|
||||
render(<ResultItem payload={payload} />)
|
||||
expect(screen.getByTestId('summary-label')).toHaveTextContent('test summary')
|
||||
})
|
||||
|
||||
it('should show chunk detail modal on click', () => {
|
||||
render(<ResultItem payload={makePayload()} />)
|
||||
fireEvent.click(screen.getByTestId('markdown'))
|
||||
expect(screen.getByTestId('chunk-detail-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render images when files exist', () => {
|
||||
const payload = makePayload({
|
||||
files: [{ name: 'img.png', mime_type: 'image/png', source_url: 'url', size: 100, extension: 'png' }],
|
||||
})
|
||||
render(<ResultItem payload={payload} />)
|
||||
expect(screen.getByTestId('image-list')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,94 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import ModifyExternalRetrievalModal from './modify-external-retrieval-modal'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiCloseLine: () => <span data-testid="close-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/action-button', () => ({
|
||||
default: ({ children, onClick }: { children: React.ReactNode, onClick: () => void }) => (
|
||||
<button data-testid="action-button" onClick={onClick}>{children}</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, variant }: { children: React.ReactNode, onClick: () => void, variant?: string }) => (
|
||||
<button data-testid={variant === 'primary' ? 'save-button' : 'cancel-button'} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../external-knowledge-base/create/RetrievalSettings', () => ({
|
||||
default: ({ topK, scoreThreshold, _scoreThresholdEnabled, onChange }: { topK: number, scoreThreshold: number, _scoreThresholdEnabled: boolean, onChange: (data: Record<string, unknown>) => void }) => (
|
||||
<div data-testid="retrieval-settings">
|
||||
<span data-testid="top-k">{topK}</span>
|
||||
<span data-testid="score-threshold">{scoreThreshold}</span>
|
||||
<button data-testid="change-top-k" onClick={() => onChange({ top_k: 10 })}>change</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('ModifyExternalRetrievalModal', () => {
|
||||
const defaultProps = {
|
||||
onClose: vi.fn(),
|
||||
onSave: vi.fn(),
|
||||
initialTopK: 4,
|
||||
initialScoreThreshold: 0.5,
|
||||
initialScoreThresholdEnabled: false,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render title', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
expect(screen.getByText('settingTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render retrieval settings with initial values', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
expect(screen.getByTestId('top-k')).toHaveTextContent('4')
|
||||
expect(screen.getByTestId('score-threshold')).toHaveTextContent('0.5')
|
||||
})
|
||||
|
||||
it('should call onClose when close button clicked', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('action-button'))
|
||||
expect(defaultProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onClose when cancel button clicked', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('cancel-button'))
|
||||
expect(defaultProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onSave with current values and close when save clicked', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
expect(defaultProps.onSave).toHaveBeenCalledWith({
|
||||
top_k: 4,
|
||||
score_threshold: 0.5,
|
||||
score_threshold_enabled: false,
|
||||
})
|
||||
expect(defaultProps.onClose).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save updated values after settings change', () => {
|
||||
render(<ModifyExternalRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('change-top-k'))
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
expect(defaultProps.onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ top_k: 10 }),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,124 @@
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import ModifyRetrievalModal from './modify-retrieval-modal'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@remixicon/react', () => ({
|
||||
RiCloseLine: () => <span data-testid="close-icon" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, variant }: { children: React.ReactNode, onClick: () => void, variant?: string }) => (
|
||||
<button data-testid={variant === 'primary' ? 'save-button' : 'cancel-button'} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({
|
||||
isReRankModelSelected: vi.fn(() => true),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/retrieval-method-config', () => ({
|
||||
default: ({ value, onChange }: { value: RetrievalConfig, onChange: (v: RetrievalConfig) => void }) => (
|
||||
<div data-testid="retrieval-method-config">
|
||||
<span>{value.search_method}</span>
|
||||
<button data-testid="change-config" onClick={() => onChange({ ...value, search_method: RETRIEVE_METHOD.hybrid })}>change</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/common/economical-retrieval-method-config', () => ({
|
||||
default: () => <div data-testid="economical-config" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelList: () => ({ data: [] }),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: () => 'model-name',
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
vi.mock('../../base/toast', () => ({
|
||||
default: { notify: vi.fn() },
|
||||
}))
|
||||
|
||||
vi.mock('../settings/utils', () => ({
|
||||
checkShowMultiModalTip: () => false,
|
||||
}))
|
||||
|
||||
describe('ModifyRetrievalModal', () => {
|
||||
const defaultProps = {
|
||||
indexMethod: 'high_quality',
|
||||
value: {
|
||||
search_method: 'semantic_search',
|
||||
reranking_enable: false,
|
||||
reranking_model: {
|
||||
reranking_provider_name: '',
|
||||
reranking_model_name: '',
|
||||
},
|
||||
} as RetrievalConfig,
|
||||
isShow: true,
|
||||
onHide: vi.fn(),
|
||||
onSave: vi.fn(),
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return null when isShow is false', () => {
|
||||
const { container } = render(<ModifyRetrievalModal {...defaultProps} isShow={false} />)
|
||||
expect(container.firstChild).toBeNull()
|
||||
})
|
||||
|
||||
it('should render title when isShow is true', () => {
|
||||
render(<ModifyRetrievalModal {...defaultProps} />)
|
||||
expect(screen.getByText('form.retrievalSetting.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render high quality retrieval config for high_quality index', () => {
|
||||
render(<ModifyRetrievalModal {...defaultProps} />)
|
||||
expect(screen.getByTestId('retrieval-method-config')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render economical config for non high_quality index', () => {
|
||||
render(<ModifyRetrievalModal {...defaultProps} indexMethod="economy" />)
|
||||
expect(screen.getByTestId('economical-config')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onHide when close button clicked', () => {
|
||||
render(<ModifyRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('close-icon'))
|
||||
expect(defaultProps.onHide).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onHide when cancel button clicked', () => {
|
||||
render(<ModifyRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('cancel-button'))
|
||||
expect(defaultProps.onHide).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onSave with retrieval config when save clicked', () => {
|
||||
render(<ModifyRetrievalModal {...defaultProps} />)
|
||||
fireEvent.click(screen.getByTestId('save-button'))
|
||||
expect(defaultProps.onSave).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render learn more link', () => {
|
||||
render(<ModifyRetrievalModal {...defaultProps} />)
|
||||
expect(screen.getByText('form.retrievalSetting.learnMore')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -3462,11 +3462,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/base/header.spec.tsx": {
|
||||
"ts/no-explicit-any": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"app/components/datasets/documents/create-from-pipeline/data-source/base/header.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
|
||||
Reference in New Issue
Block a user