test: add unit tests for various dataset components

- Introduced new test files for ChunkLabel, ChunkContainer, QAPreview, DatasetsLoading, NoLinkedAppsPanel, ApiIndex, and several document-related components.
- Enhanced test coverage by validating rendering, user interactions, and edge cases for each component.
- Ensured proper functionality of user interactions, such as rendering conditions and state management, improving the reliability and maintainability of dataset-related features.
This commit is contained in:
CodingOnStar
2026-02-11 12:31:12 +08:00
parent a9f56716fc
commit e92867884f
371 changed files with 1172 additions and 10147 deletions

View File

@ -1,9 +1,9 @@
import type { QA } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkContainer, ChunkLabel, QAPreview } from './chunk'
import { ChunkContainer, ChunkLabel, QAPreview } from '../chunk'
vi.mock('../base/icons/src/public/knowledge', () => ({
vi.mock('../../base/icons/src/public/knowledge', () => ({
SelectionMod: (props: React.ComponentProps<'svg'>) => (
<svg data-testid="selection-mod-icon" {...props} />
),

View File

@ -1,6 +1,6 @@
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import DatasetsLoading from './loading'
import DatasetsLoading from '../loading'
afterEach(() => {
cleanup()

View File

@ -1,13 +1,6 @@
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import NoLinkedAppsPanel from './no-linked-apps-panel'
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
import NoLinkedAppsPanel from '../no-linked-apps-panel'
// Mock useDocLink
vi.mock('@/context/i18n', () => ({
@ -21,17 +14,17 @@ afterEach(() => {
describe('NoLinkedAppsPanel', () => {
it('should render without crashing', () => {
render(<NoLinkedAppsPanel />)
expect(screen.getByText('datasetMenus.emptyTip')).toBeInTheDocument()
expect(screen.getByText('common.datasetMenus.emptyTip')).toBeInTheDocument()
})
it('should render the empty tip text', () => {
render(<NoLinkedAppsPanel />)
expect(screen.getByText('datasetMenus.emptyTip')).toBeInTheDocument()
expect(screen.getByText('common.datasetMenus.emptyTip')).toBeInTheDocument()
})
it('should render the view doc link', () => {
render(<NoLinkedAppsPanel />)
expect(screen.getByText('datasetMenus.viewDoc')).toBeInTheDocument()
expect(screen.getByText('common.datasetMenus.viewDoc')).toBeInTheDocument()
})
it('should render link with correct href', () => {

View File

@ -1,6 +1,6 @@
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import ApiIndex from './index'
import ApiIndex from '../index'
afterEach(() => {
cleanup()

View File

@ -4,7 +4,7 @@ import { describe, expect, it } from 'vitest'
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { RerankingModeEnum } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { ensureRerankModelSelected, isReRankModelSelected } from './check-rerank-model'
import { ensureRerankModelSelected, isReRankModelSelected } from '../check-rerank-model'
// Test data factory
const createRetrievalConfig = (overrides: Partial<RetrievalConfig> = {}): RetrievalConfig => ({

View File

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import ChunkingModeLabel from './chunking-mode-label'
import ChunkingModeLabel from '../chunking-mode-label'
describe('ChunkingModeLabel', () => {
describe('Rendering', () => {

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { CredentialIcon } from './credential-icon'
import { CredentialIcon } from '../credential-icon'
describe('CredentialIcon', () => {
describe('Rendering', () => {

View File

@ -1,6 +1,6 @@
import { render } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import DocumentFileIcon from './document-file-icon'
import DocumentFileIcon from '../document-file-icon'
describe('DocumentFileIcon', () => {
describe('Rendering', () => {

View File

@ -1,9 +1,9 @@
import type { DocumentItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import DocumentList from './document-list'
import DocumentList from '../document-list'
vi.mock('../document-file-icon', () => ({
vi.mock('../../document-file-icon', () => ({
default: ({ name, extension }: { name?: string, extension?: string }) => (
<span data-testid="file-icon">
{name}

View File

@ -3,7 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { ChunkingMode, DataSourceType } from '@/models/datasets'
import DocumentPicker from './index'
import DocumentPicker from '../index'
// Mock portal-to-follow-elem - always render content for testing
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
@ -52,25 +52,6 @@ vi.mock('@/service/knowledge/use-document', () => ({
useDocumentList: mockUseDocumentList,
}))
// Mock icons - mock all remixicon components used in the component tree
vi.mock('@remixicon/react', () => ({
RiArrowDownSLine: () => <span data-testid="arrow-icon"></span>,
RiFile3Fill: () => <span data-testid="file-icon">📄</span>,
RiFileCodeFill: () => <span data-testid="file-code-icon">📄</span>,
RiFileExcelFill: () => <span data-testid="file-excel-icon">📄</span>,
RiFileGifFill: () => <span data-testid="file-gif-icon">📄</span>,
RiFileImageFill: () => <span data-testid="file-image-icon">📄</span>,
RiFileMusicFill: () => <span data-testid="file-music-icon">📄</span>,
RiFilePdf2Fill: () => <span data-testid="file-pdf-icon">📄</span>,
RiFilePpt2Fill: () => <span data-testid="file-ppt-icon">📄</span>,
RiFileTextFill: () => <span data-testid="file-text-icon">📄</span>,
RiFileVideoFill: () => <span data-testid="file-video-icon">📄</span>,
RiFileWordFill: () => <span data-testid="file-word-icon">📄</span>,
RiMarkdownFill: () => <span data-testid="file-markdown-icon">📄</span>,
RiSearchLine: () => <span data-testid="search-icon">🔍</span>,
RiCloseLine: () => <span data-testid="close-icon"></span>,
}))
// Factory function to create mock SimpleDocumentDetail
const createMockDocument = (overrides: Partial<SimpleDocumentDetail> = {}): SimpleDocumentDetail => ({
id: `doc-${Math.random().toString(36).substr(2, 9)}`,
@ -211,12 +192,6 @@ describe('DocumentPicker', () => {
expect(screen.getByText('--')).toBeInTheDocument()
})
it('should render arrow icon', () => {
renderComponent()
expect(screen.getByTestId('arrow-icon')).toBeInTheDocument()
})
it('should render general mode label', () => {
renderComponent({
value: {
@ -473,7 +448,7 @@ describe('DocumentPicker', () => {
describe('Memoization Logic', () => {
it('should be wrapped with React.memo', () => {
// React.memo components have a $$typeof property
expect((DocumentPicker as any).$$typeof).toBeDefined()
expect((DocumentPicker as unknown as { $$typeof: symbol }).$$typeof).toBeDefined()
})
it('should compute parentModeLabel correctly with useMemo', () => {
@ -952,7 +927,6 @@ describe('DocumentPicker', () => {
renderComponent({ onChange })
// Click on a document in the list
fireEvent.click(screen.getByText('Document 2'))
// handleChange should find the document and call onChange with full document
@ -1026,8 +1000,9 @@ describe('DocumentPicker', () => {
},
})
// FileIcon should be rendered via DocumentFileIcon - pdf renders pdf icon
expect(screen.getByTestId('file-pdf-icon')).toBeInTheDocument()
// FileIcon should render an SVG icon for the file extension
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})

View File

@ -1,20 +1,7 @@
import type { DocumentItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import PreviewDocumentPicker from './preview-document-picker'
// Override shared i18n mock for custom translations
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, params?: Record<string, unknown>) => {
if (key === 'preprocessDocument' && params?.num)
return `${params.num} files`
const prefix = params?.ns ? `${params.ns}.` : ''
return `${prefix}${key}`
},
}),
}))
import PreviewDocumentPicker from '../preview-document-picker'
// Mock portal-to-follow-elem - always render content for testing
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
@ -45,23 +32,6 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
),
}))
// Mock icons
vi.mock('@remixicon/react', () => ({
RiArrowDownSLine: () => <span data-testid="arrow-icon"></span>,
RiFile3Fill: () => <span data-testid="file-icon">📄</span>,
RiFileCodeFill: () => <span data-testid="file-code-icon">📄</span>,
RiFileExcelFill: () => <span data-testid="file-excel-icon">📄</span>,
RiFileGifFill: () => <span data-testid="file-gif-icon">📄</span>,
RiFileImageFill: () => <span data-testid="file-image-icon">📄</span>,
RiFileMusicFill: () => <span data-testid="file-music-icon">📄</span>,
RiFilePdf2Fill: () => <span data-testid="file-pdf-icon">📄</span>,
RiFilePpt2Fill: () => <span data-testid="file-ppt-icon">📄</span>,
RiFileTextFill: () => <span data-testid="file-text-icon">📄</span>,
RiFileVideoFill: () => <span data-testid="file-video-icon">📄</span>,
RiFileWordFill: () => <span data-testid="file-word-icon">📄</span>,
RiMarkdownFill: () => <span data-testid="file-markdown-icon">📄</span>,
}))
// Factory function to create mock DocumentItem
const createMockDocumentItem = (overrides: Partial<DocumentItem> = {}): DocumentItem => ({
id: `doc-${Math.random().toString(36).substr(2, 9)}`,
@ -134,19 +104,14 @@ describe('PreviewDocumentPicker', () => {
expect(screen.getByText('--')).toBeInTheDocument()
})
it('should render arrow icon', () => {
renderComponent()
expect(screen.getByTestId('arrow-icon')).toBeInTheDocument()
})
it('should render file icon', () => {
renderComponent({
value: createMockDocumentItem({ extension: 'txt' }),
files: [], // Use empty files to avoid duplicate icons
})
expect(screen.getByTestId('file-text-icon')).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
it('should render pdf icon for pdf extension', () => {
@ -155,7 +120,8 @@ describe('PreviewDocumentPicker', () => {
files: [], // Use empty files to avoid duplicate icons
})
expect(screen.getByTestId('file-pdf-icon')).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})
@ -206,7 +172,8 @@ describe('PreviewDocumentPicker', () => {
value: createMockDocumentItem({ name: 'test.docx', extension: 'docx' }),
})
expect(screen.getByTestId('file-word-icon')).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})
@ -282,7 +249,7 @@ describe('PreviewDocumentPicker', () => {
// Tests for component memoization
describe('Component Memoization', () => {
it('should be wrapped with React.memo', () => {
expect((PreviewDocumentPicker as any).$$typeof).toBeDefined()
expect((PreviewDocumentPicker as unknown as { $$typeof: symbol }).$$typeof).toBeDefined()
})
it('should not re-render when props are the same', () => {
@ -329,7 +296,6 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files, onChange })
// Click on a document
fireEvent.click(screen.getByText('Document 2'))
// handleChange should call onChange with the selected item
@ -506,21 +472,16 @@ describe('PreviewDocumentPicker', () => {
})
describe('extension variations', () => {
const extensions = [
{ ext: 'txt', icon: 'file-text-icon' },
{ ext: 'pdf', icon: 'file-pdf-icon' },
{ ext: 'docx', icon: 'file-word-icon' },
{ ext: 'xlsx', icon: 'file-excel-icon' },
{ ext: 'md', icon: 'file-markdown-icon' },
]
const extensions = ['txt', 'pdf', 'docx', 'xlsx', 'md']
it.each(extensions)('should render correct icon for $ext extension', ({ ext, icon }) => {
it.each(extensions)('should render icon for %s extension', (ext) => {
renderComponent({
value: createMockDocumentItem({ extension: ext }),
files: [], // Use empty files to avoid duplicate icons
})
expect(screen.getByTestId(icon)).toBeInTheDocument()
const trigger = screen.getByTestId('portal-trigger')
expect(trigger.querySelector('svg')).toBeInTheDocument()
})
})
})
@ -543,7 +504,6 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files, onChange })
// Click on first document
fireEvent.click(screen.getByText('Document 1'))
expect(onChange).toHaveBeenCalledWith(files[0])
@ -568,7 +528,7 @@ describe('PreviewDocumentPicker', () => {
onChange={vi.fn()}
/>,
)
expect(screen.getByText('3 files')).toBeInTheDocument()
expect(screen.getByText(/dataset\.preprocessDocument/)).toBeInTheDocument()
})
})
@ -609,7 +569,6 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files, onChange })
// Click first document
fireEvent.click(screen.getByText('Document 1'))
expect(onChange).toHaveBeenCalledWith(files[0])
@ -624,11 +583,9 @@ describe('PreviewDocumentPicker', () => {
renderComponent({ files: customFiles, onChange })
// Click on first custom file
fireEvent.click(screen.getByText('Custom File 1'))
expect(onChange).toHaveBeenCalledWith(customFiles[0])
// Click on second custom file
fireEvent.click(screen.getByText('Custom File 2'))
expect(onChange).toHaveBeenCalledWith(customFiles[1])
})

View File

@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import { useAutoDisabledDocuments } from '@/service/knowledge/use-document'
import AutoDisabledDocument from './auto-disabled-document'
import AutoDisabledDocument from '../auto-disabled-document'
type AutoDisabledDocumentsResponse = { document_ids: string[] }
@ -15,7 +15,6 @@ const createMockQueryResult = (
isLoading,
}) as ReturnType<typeof useAutoDisabledDocuments>
// Mock service hooks
const mockMutateAsync = vi.fn()
const mockInvalidDisabledDocument = vi.fn()
@ -27,7 +26,6 @@ vi.mock('@/service/knowledge/use-document', () => ({
useInvalidDisabledDocument: vi.fn(() => mockInvalidDisabledDocument),
}))
// Mock Toast
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: vi.fn(),

View File

@ -3,9 +3,8 @@ import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/re
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { retryErrorDocs } from '@/service/datasets'
import { useDatasetErrorDocs } from '@/service/knowledge/use-dataset'
import RetryButton from './index-failed'
import RetryButton from '../index-failed'
// Mock service hooks
const mockRefetch = vi.fn()
vi.mock('@/service/knowledge/use-dataset', () => ({

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import StatusWithAction from './status-with-action'
import StatusWithAction from '../status-with-action'
describe('StatusWithAction', () => {
describe('Rendering', () => {

View File

@ -1,9 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { RETRIEVE_METHOD } from '@/types/app'
import EconomicalRetrievalMethodConfig from './index'
import EconomicalRetrievalMethodConfig from '../index'
// Mock dependencies
vi.mock('../../settings/option-card', () => ({
vi.mock('../../../settings/option-card', () => ({
default: ({ children, title, description, disabled, id }: {
children?: React.ReactNode
title?: string
@ -18,7 +17,7 @@ vi.mock('../../settings/option-card', () => ({
),
}))
vi.mock('../retrieval-param-config', () => ({
vi.mock('../../retrieval-param-config', () => ({
default: ({ value, onChange, type }: {
value: Record<string, unknown>
onChange: (value: Record<string, unknown>) => void

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import ImageList from './index'
import ImageList from '../index'
// Track handleImageClick calls for testing
type FileEntity = {
@ -43,7 +43,7 @@ type ImageInfo = {
}
// Mock ImagePreviewer since it uses createPortal
vi.mock('../image-previewer', () => ({
vi.mock('../../image-previewer', () => ({
default: ({ images, initialIndex, onClose }: ImagePreviewerProps) => (
<div data-testid="image-previewer">
<span data-testid="preview-count">{images.length}</span>
@ -132,7 +132,6 @@ describe('ImageList', () => {
const images = createMockImages(15)
render(<ImageList images={images} size="md" limit={9} />)
// Click More button
const moreButton = screen.getByText(/\+6/)
fireEvent.click(moreButton)
@ -182,7 +181,6 @@ describe('ImageList', () => {
const images = createMockImages(3)
const { rerender } = render(<ImageList images={images} size="md" />)
// Click first image to open preview
const firstThumb = screen.getByTestId('file-thumb-https://example.com/image-1.png')
fireEvent.click(firstThumb)
@ -197,7 +195,6 @@ describe('ImageList', () => {
const newImages = createMockImages(2) // Only 2 images
rerender(<ImageList images={newImages} size="md" />)
// Click on a thumbnail that exists
const validThumb = screen.getByTestId('file-thumb-https://example.com/image-1.png')
fireEvent.click(validThumb)
expect(screen.getByTestId('image-previewer')).toBeInTheDocument()

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import More from './more'
import More from '../more'
describe('More', () => {
describe('Rendering', () => {

View File

@ -1,6 +1,6 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import ImagePreviewer from './index'
import ImagePreviewer from '../index'
// Mock fetch
const mockFetch = vi.fn()
@ -12,7 +12,6 @@ const mockCreateObjectURL = vi.fn(() => 'blob:mock-url')
globalThis.URL.revokeObjectURL = mockRevokeObjectURL
globalThis.URL.createObjectURL = mockCreateObjectURL
// Mock Image
class MockImage {
onload: (() => void) | null = null
onerror: (() => void) | null = null
@ -294,7 +293,6 @@ describe('ImagePreviewer', () => {
expect(screen.getByText('image1.png')).toBeInTheDocument()
})
// Click prev button multiple times - should stay at first image
const buttons = document.querySelectorAll('button')
const prevButton = Array.from(buttons).find(btn =>
btn.className.includes('left-8'),
@ -325,7 +323,6 @@ describe('ImagePreviewer', () => {
expect(screen.getByText('image3.png')).toBeInTheDocument()
})
// Click next button multiple times - should stay at last image
const buttons = document.querySelectorAll('button')
const nextButton = Array.from(buttons).find(btn =>
btn.className.includes('right-8'),
@ -372,7 +369,6 @@ describe('ImagePreviewer', () => {
expect(screen.getByText(/Failed to load image/)).toBeInTheDocument()
})
// Click retry button
const retryButton = document.querySelector('button.rounded-full')
if (retryButton) {
await act(async () => {

View File

@ -1,4 +1,4 @@
import type { FileEntity } from './types'
import type { FileEntity } from '../types'
import { act, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import {
@ -6,7 +6,7 @@ import {
FileContextProvider,
useFileStore,
useFileStoreWithSelector,
} from './store'
} from '../store'
const createMockFile = (id: string): FileEntity => ({
id,

View File

@ -1,12 +1,12 @@
import type { FileEntity } from './types'
import type { FileEntity } from '../types'
import type { FileUploadConfigResponse } from '@/models/common'
import { describe, expect, it } from 'vitest'
import {
DEFAULT_IMAGE_FILE_BATCH_LIMIT,
DEFAULT_IMAGE_FILE_SIZE_LIMIT,
DEFAULT_SINGLE_CHUNK_ATTACHMENT_LIMIT,
} from './constants'
import { fileIsUploaded, getFileType, getFileUploadConfig, traverseFileEntry } from './utils'
} from '../constants'
import { fileIsUploaded, getFileType, getFileUploadConfig, traverseFileEntry } from '../utils'
describe('image-uploader utils', () => {
describe('getFileType', () => {

View File

@ -1,13 +1,12 @@
import type { PropsWithChildren } from 'react'
import type { FileEntity } from '../types'
import type { FileEntity } from '../../types'
import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import { FileContextProvider } from '../store'
import { useUpload } from './use-upload'
import { FileContextProvider } from '../../store'
import { useUpload } from '../use-upload'
// Mock dependencies
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: vi.fn(() => ({
data: {

View File

@ -1,9 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { FileContextProvider } from '../store'
import ImageInput from './image-input'
import { FileContextProvider } from '../../store'
import ImageInput from '../image-input'
// Mock dependencies
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: vi.fn(() => ({
data: {

View File

@ -1,7 +1,7 @@
import type { FileEntity } from '../types'
import type { FileEntity } from '../../types'
import { fireEvent, render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import ImageItem from './image-item'
import ImageItem from '../image-item'
const createMockFile = (overrides: Partial<FileEntity> = {}): FileEntity => ({
id: 'test-id',

View File

@ -1,9 +1,8 @@
import type { FileEntity } from '../types'
import type { FileEntity } from '../../types'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import ImageUploaderInChunkWrapper from './index'
import ImageUploaderInChunkWrapper from '../index'
// Mock dependencies
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: vi.fn(() => ({
data: {

View File

@ -1,10 +1,9 @@
import type { FileEntity } from '../types'
import type { FileEntity } from '../../types'
import { fireEvent, render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { FileContextProvider } from '../store'
import ImageInput from './image-input'
import { FileContextProvider } from '../../store'
import ImageInput from '../image-input'
// Mock dependencies
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: vi.fn(() => ({
data: {

View File

@ -1,7 +1,7 @@
import type { FileEntity } from '../types'
import type { FileEntity } from '../../types'
import { fireEvent, render } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import ImageItem from './image-item'
import ImageItem from '../image-item'
const createMockFile = (overrides: Partial<FileEntity> = {}): FileEntity => ({
id: 'test-id',

View File

@ -1,9 +1,8 @@
import type { FileEntity } from '../types'
import type { FileEntity } from '../../types'
import { fireEvent, render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import ImageUploaderInRetrievalTestingWrapper from './index'
import ImageUploaderInRetrievalTestingWrapper from '../index'
// Mock dependencies
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: vi.fn(() => ({
data: {

View File

@ -7,7 +7,7 @@ import {
WeightedScoreEnum,
} from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import RetrievalMethodConfig from './index'
import RetrievalMethodConfig from '../index'
// Mock provider context with controllable supportRetrievalMethods
let mockSupportRetrievalMethods: RETRIEVE_METHOD[] = [
@ -37,7 +37,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
}))
// Mock child component RetrievalParamConfig to simplify testing
vi.mock('../retrieval-param-config', () => ({
vi.mock('../../retrieval-param-config', () => ({
default: ({ type, value, onChange, showMultiModalTip }: {
type: RETRIEVE_METHOD
value: RetrievalConfig
@ -585,7 +585,7 @@ describe('RetrievalMethodConfig', () => {
// Verify the component is wrapped with React.memo by checking its displayName or type
expect(RetrievalMethodConfig).toBeDefined()
// React.memo components have a $$typeof property
expect((RetrievalMethodConfig as any).$$typeof).toBeDefined()
expect((RetrievalMethodConfig as unknown as { $$typeof: symbol }).$$typeof).toBeDefined()
})
it('should not re-render when props are the same', () => {

View File

@ -1,10 +1,10 @@
import type { ReactNode } from 'react'
import { render, screen } from '@testing-library/react'
import { RETRIEVE_METHOD } from '@/types/app'
import { retrievalIcon } from '../../create/icons'
import RetrievalMethodInfo, { getIcon } from './index'
import { retrievalIcon } from '../../../create/icons'
import RetrievalMethodInfo, { getIcon } from '../index'
// Mock next/image
// Override global next/image auto-mock: tests assert on rendered <img> src attributes via data-testid
vi.mock('next/image', () => ({
default: ({ src, alt, className }: { src: string, alt: string, className?: string }) => (
<img src={src} alt={alt || ''} className={className} data-testid="method-icon" />
@ -24,7 +24,7 @@ vi.mock('@/app/components/base/radio-card', () => ({
}))
// Mock icons
vi.mock('../../create/icons', () => ({
vi.mock('../../../create/icons', () => ({
retrievalIcon: {
vector: 'vector-icon.png',
fullText: 'fulltext-icon.png',

View File

@ -2,13 +2,7 @@ import type { RetrievalConfig } from '@/types/app'
import { fireEvent, render, screen } from '@testing-library/react'
import { RerankingModeEnum, WeightedScoreEnum } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import RetrievalParamConfig from './index'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
import RetrievalParamConfig from '../index'
const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
@ -268,7 +262,7 @@ describe('RetrievalParamConfig', () => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
message: 'errorMsg.rerankModelRequired',
message: 'workflow.errorMsg.rerankModelRequired',
})
})
@ -358,7 +352,7 @@ describe('RetrievalParamConfig', () => {
/>,
)
expect(screen.getByText('form.retrievalSetting.multiModalTip')).toBeInTheDocument()
expect(screen.getByText('datasetSettings.form.retrievalSetting.multiModalTip')).toBeInTheDocument()
})
it('should not show multimodal tip when showMultiModalTip is false', () => {
@ -372,7 +366,7 @@ describe('RetrievalParamConfig', () => {
/>,
)
expect(screen.queryByText('form.retrievalSetting.multiModalTip')).not.toBeInTheDocument()
expect(screen.queryByText('datasetSettings.form.retrievalSetting.multiModalTip')).not.toBeInTheDocument()
})
})
@ -505,7 +499,7 @@ describe('RetrievalParamConfig', () => {
/>,
)
expect(screen.getByText('weightedScore.title')).toBeInTheDocument()
expect(screen.getByText('dataset.weightedScore.title')).toBeInTheDocument()
})
it('should have RerankingModel option', () => {
@ -517,7 +511,7 @@ describe('RetrievalParamConfig', () => {
/>,
)
expect(screen.getByText('modelProvider.rerankModel.key')).toBeInTheDocument()
expect(screen.getByText('common.modelProvider.rerankModel.key')).toBeInTheDocument()
})
it('should show model selector when RerankingModel mode is selected', () => {
@ -570,7 +564,7 @@ describe('RetrievalParamConfig', () => {
)
const radioCards = screen.getAllByTestId('radio-card')
const weightedScoreCard = radioCards.find(card => card.getAttribute('data-title') === 'weightedScore.title')
const weightedScoreCard = radioCards.find(card => card.getAttribute('data-title') === 'dataset.weightedScore.title')
fireEvent.click(weightedScoreCard!)
expect(mockOnChange).toHaveBeenCalled()
@ -589,7 +583,7 @@ describe('RetrievalParamConfig', () => {
)
const radioCards = screen.getAllByTestId('radio-card')
const rerankModelCard = radioCards.find(card => card.getAttribute('data-title') === 'modelProvider.rerankModel.key')
const rerankModelCard = radioCards.find(card => card.getAttribute('data-title') === 'common.modelProvider.rerankModel.key')
fireEvent.click(rerankModelCard!)
expect(mockOnChange).not.toHaveBeenCalled()
@ -621,12 +615,12 @@ describe('RetrievalParamConfig', () => {
)
const radioCards = screen.getAllByTestId('radio-card')
const rerankModelCard = radioCards.find(card => card.getAttribute('data-title') === 'modelProvider.rerankModel.key')
const rerankModelCard = radioCards.find(card => card.getAttribute('data-title') === 'common.modelProvider.rerankModel.key')
fireEvent.click(rerankModelCard!)
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
message: 'errorMsg.rerankModelRequired',
message: 'workflow.errorMsg.rerankModelRequired',
})
})
@ -736,7 +730,7 @@ describe('RetrievalParamConfig', () => {
/>,
)
expect(screen.getByText('form.retrievalSetting.multiModalTip')).toBeInTheDocument()
expect(screen.getByText('datasetSettings.form.retrievalSetting.multiModalTip')).toBeInTheDocument()
})
it('should not show multimodal tip for hybrid search with WeightedScore', () => {
@ -764,7 +758,7 @@ describe('RetrievalParamConfig', () => {
/>,
)
expect(screen.queryByText('form.retrievalSetting.multiModalTip')).not.toBeInTheDocument()
expect(screen.queryByText('datasetSettings.form.retrievalSetting.multiModalTip')).not.toBeInTheDocument()
})
it('should not render rerank switch for hybrid search', () => {
@ -826,7 +820,7 @@ describe('RetrievalParamConfig', () => {
/>,
)
expect(screen.getByText('modelProvider.rerankModel.key')).toBeInTheDocument()
expect(screen.getByText('common.modelProvider.rerankModel.key')).toBeInTheDocument()
})
})
@ -846,7 +840,7 @@ describe('RetrievalParamConfig', () => {
)
const radioCards = screen.getAllByTestId('radio-card')
const weightedScoreCard = radioCards.find(card => card.getAttribute('data-title') === 'weightedScore.title')
const weightedScoreCard = radioCards.find(card => card.getAttribute('data-title') === 'dataset.weightedScore.title')
fireEvent.click(weightedScoreCard!)
expect(mockOnChange).toHaveBeenCalled()
@ -880,7 +874,7 @@ describe('RetrievalParamConfig', () => {
)
const radioCards = screen.getAllByTestId('radio-card')
const weightedScoreCard = radioCards.find(card => card.getAttribute('data-title') === 'weightedScore.title')
const weightedScoreCard = radioCards.find(card => card.getAttribute('data-title') === 'dataset.weightedScore.title')
fireEvent.click(weightedScoreCard!)
expect(mockOnChange).toHaveBeenCalled()

View File

@ -1,19 +1,17 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Footer from './footer'
import Footer from '../footer'
// Configurable mock for search params
let mockSearchParams = new URLSearchParams()
const mockReplace = vi.fn()
// Mock next/navigation
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: mockReplace }),
useSearchParams: () => mockSearchParams,
}))
// Mock service hook
const mockInvalidDatasetList = vi.fn()
vi.mock('@/service/knowledge/use-dataset', () => ({
useInvalidDatasetList: () => mockInvalidDatasetList,
@ -23,7 +21,7 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
let capturedActiveTab: string | undefined
let capturedDslUrl: string | undefined
vi.mock('./create-options/create-from-dsl-modal', () => ({
vi.mock('../create-options/create-from-dsl-modal', () => ({
default: ({ show, onClose, onSuccess, activeTab, dslUrl }: {
show: boolean
onClose: () => void
@ -48,9 +46,7 @@ vi.mock('./create-options/create-from-dsl-modal', () => ({
},
}))
// ============================================================================
// Footer Component Tests
// ============================================================================
describe('Footer', () => {
beforeEach(() => {
@ -60,9 +56,6 @@ describe('Footer', () => {
capturedDslUrl = undefined
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Footer />)
@ -88,9 +81,6 @@ describe('Footer', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should open modal when import button is clicked', () => {
render(<Footer />)
@ -104,12 +94,10 @@ describe('Footer', () => {
it('should close modal when onClose is called', () => {
render(<Footer />)
// Open modal
const importButton = screen.getByText(/importDSL/i)
fireEvent.click(importButton)
expect(screen.getByTestId('dsl-modal')).toBeInTheDocument()
// Close modal
const closeButton = screen.getByTestId('close-modal')
fireEvent.click(closeButton)
expect(screen.queryByTestId('dsl-modal')).not.toBeInTheDocument()
@ -118,7 +106,6 @@ describe('Footer', () => {
it('should call invalidDatasetList on success', () => {
render(<Footer />)
// Open modal
const importButton = screen.getByText(/importDSL/i)
fireEvent.click(importButton)
@ -130,9 +117,6 @@ describe('Footer', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container classes', () => {
const { container } = render(<Footer />)
@ -147,9 +131,6 @@ describe('Footer', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Footer />)
@ -158,9 +139,7 @@ describe('Footer', () => {
})
})
// --------------------------------------------------------------------------
// URL Parameter Tests (Branch Coverage)
// --------------------------------------------------------------------------
describe('URL Parameter Handling', () => {
it('should set activeTab to FROM_URL when dslUrl is present', () => {
mockSearchParams = new URLSearchParams('remoteInstallUrl=https://example.com/dsl')
@ -193,12 +172,10 @@ describe('Footer', () => {
render(<Footer />)
// Open modal
const importButton = screen.getByText(/importDSL/i)
fireEvent.click(importButton)
expect(screen.getByTestId('dsl-modal')).toBeInTheDocument()
// Close modal
const closeButton = screen.getByTestId('close-modal')
fireEvent.click(closeButton)
@ -210,11 +187,9 @@ describe('Footer', () => {
render(<Footer />)
// Open modal
const importButton = screen.getByText(/importDSL/i)
fireEvent.click(importButton)
// Close modal
const closeButton = screen.getByTestId('close-modal')
fireEvent.click(closeButton)

View File

@ -1,15 +1,10 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import Header from './header'
import Header from '../header'
// ============================================================================
// Header Component Tests
// ============================================================================
describe('Header', () => {
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Header />)
@ -41,9 +36,6 @@ describe('Header', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container classes', () => {
const { container } = render(<Header />)
@ -58,9 +50,6 @@ describe('Header', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Header />)

View File

@ -1,35 +1,30 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import CreateFromPipeline from './index'
import CreateFromPipeline from '../index'
// Mock child components to isolate testing
vi.mock('./header', () => ({
vi.mock('../header', () => ({
default: () => <div data-testid="mock-header">Header</div>,
}))
vi.mock('./list', () => ({
vi.mock('../list', () => ({
default: () => <div data-testid="mock-list">List</div>,
}))
vi.mock('./footer', () => ({
vi.mock('../footer', () => ({
default: () => <div data-testid="mock-footer">Footer</div>,
}))
vi.mock('../../base/effect', () => ({
vi.mock('../../../base/effect', () => ({
default: ({ className }: { className?: string }) => (
<div data-testid="mock-effect" className={className}>Effect</div>
),
}))
// ============================================================================
// CreateFromPipeline Component Tests
// ============================================================================
describe('CreateFromPipeline', () => {
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<CreateFromPipeline />)
@ -57,9 +52,6 @@ describe('CreateFromPipeline', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container classes', () => {
const { container } = render(<CreateFromPipeline />)
@ -86,9 +78,7 @@ describe('CreateFromPipeline', () => {
})
})
// --------------------------------------------------------------------------
// Component Order Tests
// --------------------------------------------------------------------------
describe('Component Order', () => {
it('should render components in correct order', () => {
const { container } = render(<CreateFromPipeline />)

View File

@ -1,11 +1,9 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import DSLConfirmModal from './dsl-confirm-modal'
import DSLConfirmModal from '../dsl-confirm-modal'
// ============================================================================
// DSLConfirmModal Component Tests
// ============================================================================
describe('DSLConfirmModal', () => {
const defaultProps = {
@ -17,9 +15,6 @@ describe('DSLConfirmModal', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<DSLConfirmModal {...defaultProps} />)
@ -50,9 +45,7 @@ describe('DSLConfirmModal', () => {
})
})
// --------------------------------------------------------------------------
// Versions Display Tests
// --------------------------------------------------------------------------
describe('Versions Display', () => {
it('should display imported version when provided', () => {
render(
@ -81,9 +74,6 @@ describe('DSLConfirmModal', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onCancel when cancel button is clicked', () => {
render(<DSLConfirmModal {...defaultProps} />)
@ -114,9 +104,7 @@ describe('DSLConfirmModal', () => {
})
})
// --------------------------------------------------------------------------
// Button State Tests
// --------------------------------------------------------------------------
describe('Button State', () => {
it('should enable confirm button by default', () => {
render(<DSLConfirmModal {...defaultProps} />)
@ -140,9 +128,6 @@ describe('DSLConfirmModal', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have button container with proper styling', () => {
render(<DSLConfirmModal {...defaultProps} />)

View File

@ -1,11 +1,9 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Header from './header'
import Header from '../header'
// ============================================================================
// Header Component Tests
// ============================================================================
describe('Header', () => {
const defaultProps = {
@ -16,9 +14,6 @@ describe('Header', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Header {...defaultProps} />)
@ -43,9 +38,6 @@ describe('Header', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onClose when close button is clicked', () => {
const { container } = render(<Header {...defaultProps} />)
@ -57,9 +49,6 @@ describe('Header', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container styling', () => {
const { container } = render(<Header {...defaultProps} />)
@ -80,9 +69,6 @@ describe('Header', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Header {...defaultProps} />)

View File

@ -1,13 +1,12 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import DSLConfirmModal from './dsl-confirm-modal'
import Header from './header'
import CreateFromDSLModal, { CreateFromDSLModalTab } from './index'
import Tab from './tab'
import TabItem from './tab/item'
import Uploader from './uploader'
import DSLConfirmModal from '../dsl-confirm-modal'
import Header from '../header'
import CreateFromDSLModal, { CreateFromDSLModalTab } from '../index'
import Tab from '../tab'
import TabItem from '../tab/item'
import Uploader from '../uploader'
// Mock next/navigation
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
@ -15,7 +14,6 @@ vi.mock('next/navigation', () => ({
}),
}))
// Mock service hooks
const mockImportDSL = vi.fn()
const mockImportDSLConfirm = vi.fn()
@ -37,7 +35,6 @@ vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
}),
}))
// Mock toast context
const mockNotify = vi.fn()
vi.mock('use-context-selector', async () => {
@ -48,7 +45,6 @@ vi.mock('use-context-selector', async () => {
}
})
// Test data builders
const createMockFile = (name = 'test.pipeline'): File => {
return new File(['test content'], name, { type: 'application/octet-stream' })
}
@ -88,9 +84,6 @@ describe('CreateFromDSLModal', () => {
mockHandleCheckPluginDependencies.mockReset()
})
// ============================================
// Rendering Tests
// ============================================
describe('Rendering', () => {
it('should render without crashing when show is true', () => {
render(
@ -172,9 +165,6 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// Props Testing
// ============================================
describe('Props', () => {
it('should use FROM_FILE as default activeTab', () => {
render(
@ -232,9 +222,6 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// State Management Tests
// ============================================
describe('State Management', () => {
it('should switch between tabs', () => {
render(
@ -248,7 +235,6 @@ describe('CreateFromDSLModal', () => {
// Initially file tab is active
expect(screen.getByText('app.dslUploader.button')).toBeInTheDocument()
// Click URL tab
fireEvent.click(screen.getByText('app.importFromDSLUrl'))
// URL input should be visible
@ -317,9 +303,7 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// API Call Tests
// ============================================
describe('API Calls', () => {
it('should call importDSL with URL mode when URL tab is active', async () => {
mockImportDSL.mockResolvedValue(createImportDSLResponse())
@ -526,9 +510,7 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// Event Handler Tests
// ============================================
describe('Event Handlers', () => {
it('should call onClose when header close button is clicked', () => {
const onClose = vi.fn()
@ -638,7 +620,6 @@ describe('CreateFromDSLModal', () => {
const importButton = screen.getByText('app.newApp.import').closest('button')!
// Click multiple times rapidly
fireEvent.click(importButton)
fireEvent.click(importButton)
fireEvent.click(importButton)
@ -650,9 +631,6 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// Memoization Tests
// ============================================
describe('Memoization', () => {
it('should correctly compute buttonDisabled based on currentTab and file/URL', () => {
render(
@ -684,9 +662,6 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// Edge Cases Tests
// ============================================
describe('Edge Cases', () => {
it('should handle empty URL gracefully', () => {
render(
@ -842,9 +817,7 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// File Import Tests (covers readFile, handleFile, file mode import)
// ============================================
describe('File Import', () => {
it('should read file content when file is selected', async () => {
mockImportDSL.mockResolvedValue(createImportDSLResponse())
@ -877,7 +850,6 @@ describe('CreateFromDSLModal', () => {
expect(importButton).not.toBeDisabled()
})
// Click import button
const importButton = screen.getByText('app.newApp.import').closest('button')!
fireEvent.click(importButton)
@ -927,9 +899,7 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// DSL Confirm Flow Tests (covers onDSLConfirm)
// ============================================
describe('DSL Confirm Flow', () => {
it('should handle DSL confirm success', async () => {
vi.useFakeTimers({ shouldAdvanceTime: true })
@ -978,7 +948,6 @@ describe('CreateFromDSLModal', () => {
vi.advanceTimersByTime(400)
})
// Click confirm button in error modal
await waitFor(() => {
expect(screen.getByText('app.newApp.Confirm')).toBeInTheDocument()
})
@ -1027,7 +996,6 @@ describe('CreateFromDSLModal', () => {
vi.advanceTimersByTime(400)
})
// Click confirm - should return early since importId is empty
await waitFor(() => {
expect(screen.getByText('app.newApp.Confirm')).toBeInTheDocument()
})
@ -1163,7 +1131,6 @@ describe('CreateFromDSLModal', () => {
// There are two Cancel buttons now (one in main modal footer, one in error modal)
// Find the Cancel button in the error modal context
const cancelButtons = screen.getAllByText('app.newApp.Cancel')
// Click the last Cancel button (the one in the error modal)
fireEvent.click(cancelButtons[cancelButtons.length - 1])
vi.useRealTimers()
@ -1171,9 +1138,7 @@ describe('CreateFromDSLModal', () => {
})
})
// ============================================
// Header Component Tests
// ============================================
describe('Header', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -1206,9 +1171,7 @@ describe('Header', () => {
})
})
// ============================================
// Tab Component Tests
// ============================================
describe('Tab', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -1261,9 +1224,7 @@ describe('Tab', () => {
})
})
// ============================================
// Tab Item Component Tests
// ============================================
describe('TabItem', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -1353,9 +1314,7 @@ describe('TabItem', () => {
})
})
// ============================================
// Uploader Component Tests
// ============================================
describe('Uploader', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -1679,7 +1638,6 @@ describe('Uploader', () => {
// After click, oncancel should be set
})
// Click browse link to trigger selectHandle
const browseLink = screen.getByText('app.dslUploader.browse')
fireEvent.click(browseLink)
@ -1755,9 +1713,7 @@ describe('Uploader', () => {
})
})
// ============================================
// DSLConfirmModal Component Tests
// ============================================
describe('DSLConfirmModal', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -1923,9 +1879,6 @@ describe('DSLConfirmModal', () => {
})
})
// ============================================
// Integration Tests
// ============================================
describe('CreateFromDSLModal Integration', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -1958,7 +1911,6 @@ describe('CreateFromDSLModal Integration', () => {
const input = screen.getByPlaceholderText('app.importFromDSLUrlPlaceholder')
fireEvent.change(input, { target: { value: 'https://example.com/pipeline.yaml' } })
// Click import
const importButton = screen.getByText('app.newApp.import').closest('button')!
fireEvent.click(importButton)
@ -1999,7 +1951,6 @@ describe('CreateFromDSLModal Integration', () => {
const input = screen.getByPlaceholderText('app.importFromDSLUrlPlaceholder')
fireEvent.change(input, { target: { value: 'https://example.com/old-pipeline.yaml' } })
// Click import
const importButton = screen.getByText('app.newApp.import').closest('button')!
fireEvent.click(importButton)

View File

@ -1,9 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Uploader from './uploader'
import Uploader from '../uploader'
// Mock ToastContext
const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
ToastContext: {
@ -17,17 +16,11 @@ vi.mock('use-context-selector', () => ({
useContext: () => ({ notify: mockNotify }),
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createMockFile = (name = 'test.pipeline', _size = 1024): File => {
return new File(['test content'], name, { type: 'application/octet-stream' })
}
// ============================================================================
// Uploader Component Tests
// ============================================================================
describe('Uploader', () => {
const defaultProps = {
@ -39,9 +32,7 @@ describe('Uploader', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests - No File
// --------------------------------------------------------------------------
describe('Rendering - No File', () => {
it('should render without crashing', () => {
render(<Uploader {...defaultProps} />)
@ -78,9 +69,7 @@ describe('Uploader', () => {
})
})
// --------------------------------------------------------------------------
// Rendering Tests - With File
// --------------------------------------------------------------------------
describe('Rendering - With File', () => {
it('should render file name when file is provided', () => {
const file = createMockFile('my-pipeline.pipeline')
@ -109,9 +98,6 @@ describe('Uploader', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should open file dialog when browse is clicked', () => {
render(<Uploader {...defaultProps} />)
@ -151,9 +137,7 @@ describe('Uploader', () => {
})
})
// --------------------------------------------------------------------------
// Custom className Tests
// --------------------------------------------------------------------------
describe('Custom className', () => {
it('should apply custom className', () => {
const { container } = render(<Uploader {...defaultProps} className="custom-class" />)
@ -168,9 +152,6 @@ describe('Uploader', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container styling', () => {
const { container } = render(<Uploader {...defaultProps} />)
@ -192,9 +173,6 @@ describe('Uploader', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Uploader {...defaultProps} />)

View File

@ -2,9 +2,8 @@ import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CreateFromDSLModalTab, useDSLImport } from './use-dsl-import'
import { CreateFromDSLModalTab, useDSLImport } from '../use-dsl-import'
// Mock next/navigation
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({
@ -12,7 +11,6 @@ vi.mock('next/navigation', () => ({
}),
}))
// Mock service hooks
const mockImportDSL = vi.fn()
const mockImportDSLConfirm = vi.fn()
@ -34,7 +32,6 @@ vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
}),
}))
// Mock toast context
const mockNotify = vi.fn()
vi.mock('use-context-selector', async () => {
@ -45,7 +42,6 @@ vi.mock('use-context-selector', async () => {
}
})
// Test data builders
const createImportDSLResponse = (overrides = {}) => ({
id: 'import-123',
status: 'completed' as const,

View File

@ -2,11 +2,9 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CreateFromDSLModalTab } from '@/app/components/app/create-from-dsl-modal'
import Tab from './index'
import Tab from '../index'
// ============================================================================
// Tab Component Tests
// ============================================================================
describe('Tab', () => {
const defaultProps = {
@ -18,9 +16,6 @@ describe('Tab', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Tab {...defaultProps} />)
@ -44,9 +39,7 @@ describe('Tab', () => {
})
})
// --------------------------------------------------------------------------
// Active State Tests
// --------------------------------------------------------------------------
describe('Active State', () => {
it('should mark file tab as active when currentTab is FROM_FILE', () => {
const { container } = render(
@ -65,9 +58,6 @@ describe('Tab', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call setCurrentTab with FROM_FILE when file tab is clicked', () => {
render(<Tab {...defaultProps} currentTab={CreateFromDSLModalTab.FROM_URL} />)
@ -96,9 +86,6 @@ describe('Tab', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container styling', () => {
const { container } = render(<Tab {...defaultProps} />)

View File

@ -1,11 +1,9 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Item from './item'
import Item from '../item'
// ============================================================================
// Item Component Tests
// ============================================================================
describe('Item', () => {
const defaultProps = {
@ -18,9 +16,6 @@ describe('Item', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Item {...defaultProps} />)
@ -45,9 +40,7 @@ describe('Item', () => {
})
})
// --------------------------------------------------------------------------
// Active State Tests
// --------------------------------------------------------------------------
describe('Active State', () => {
it('should have tertiary text color when inactive', () => {
const { container } = render(<Item {...defaultProps} isActive={false} />)
@ -68,9 +61,6 @@ describe('Item', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onClick when clicked', () => {
render(<Item {...defaultProps} />)
@ -88,9 +78,6 @@ describe('Item', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container styling', () => {
const { container } = render(<Item {...defaultProps} />)
@ -99,9 +86,6 @@ describe('Item', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Item {...defaultProps} />)

View File

@ -1,14 +1,13 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import BuiltInPipelineList from './built-in-pipeline-list'
import BuiltInPipelineList from '../built-in-pipeline-list'
// Mock child components
vi.mock('./create-card', () => ({
vi.mock('../create-card', () => ({
default: () => <div data-testid="create-card">CreateCard</div>,
}))
vi.mock('./template-card', () => ({
vi.mock('../template-card', () => ({
default: ({ type, pipeline, showMoreOperations }: { type: string, pipeline: { name: string }, showMoreOperations?: boolean }) => (
<div data-testid="template-card" data-type={type} data-show-more={String(showMoreOperations)}>
{pipeline.name}
@ -19,7 +18,6 @@ vi.mock('./template-card', () => ({
// Configurable locale mock
let mockLocale = 'en-US'
// Mock hooks
vi.mock('@/context/i18n', () => ({
useLocale: () => mockLocale,
}))
@ -36,9 +34,7 @@ vi.mock('@/service/use-pipeline', () => ({
usePipelineTemplateList: (...args: unknown[]) => mockUsePipelineTemplateList(...args),
}))
// ============================================================================
// BuiltInPipelineList Component Tests
// ============================================================================
describe('BuiltInPipelineList', () => {
beforeEach(() => {
@ -46,9 +42,6 @@ describe('BuiltInPipelineList', () => {
mockLocale = 'en-US'
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -71,9 +64,7 @@ describe('BuiltInPipelineList', () => {
})
})
// --------------------------------------------------------------------------
// Loading State Tests
// --------------------------------------------------------------------------
describe('Loading State', () => {
it('should not render TemplateCards when loading', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -88,9 +79,7 @@ describe('BuiltInPipelineList', () => {
})
})
// --------------------------------------------------------------------------
// Rendering with Data Tests
// --------------------------------------------------------------------------
describe('Rendering with Data', () => {
it('should render TemplateCard for each pipeline when not loading', () => {
const mockPipelines = [
@ -136,9 +125,7 @@ describe('BuiltInPipelineList', () => {
})
})
// --------------------------------------------------------------------------
// API Call Tests
// --------------------------------------------------------------------------
describe('API Call', () => {
it('should call usePipelineTemplateList with type built-in', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -154,9 +141,6 @@ describe('BuiltInPipelineList', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have grid layout', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -181,9 +165,7 @@ describe('BuiltInPipelineList', () => {
})
})
// --------------------------------------------------------------------------
// Locale Handling Tests (Branch Coverage)
// --------------------------------------------------------------------------
describe('Locale Handling', () => {
it('should use zh-Hans locale when set', () => {
mockLocale = 'zh-Hans'
@ -247,9 +229,7 @@ describe('BuiltInPipelineList', () => {
})
})
// --------------------------------------------------------------------------
// Empty Data Tests
// --------------------------------------------------------------------------
describe('Empty Data', () => {
it('should handle null pipeline_templates', () => {
mockUsePipelineTemplateList.mockReturnValue({

View File

@ -1,9 +1,8 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CreateCard from './create-card'
import CreateCard from '../create-card'
// Mock next/navigation
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: mockPush }),
@ -14,14 +13,12 @@ vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: vi.fn(),
}))
// Mock Toast
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: vi.fn(),
},
}))
// Mock service hooks
const mockCreateEmptyDataset = vi.fn()
const mockInvalidDatasetList = vi.fn()
@ -35,18 +32,13 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
useInvalidDatasetList: () => mockInvalidDatasetList,
}))
// ============================================================================
// CreateCard Component Tests
// ============================================================================
describe('CreateCard', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<CreateCard />)
@ -66,9 +58,6 @@ describe('CreateCard', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call createEmptyDataset when clicked', async () => {
mockCreateEmptyDataset.mockImplementation((_data, callbacks) => {
@ -154,9 +143,6 @@ describe('CreateCard', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper card styling', () => {
const { container } = render(<CreateCard />)
@ -177,9 +163,6 @@ describe('CreateCard', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<CreateCard />)

View File

@ -1,10 +1,10 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CustomizedList from './customized-list'
import CustomizedList from '../customized-list'
// Mock TemplateCard
vi.mock('./template-card', () => ({
vi.mock('../template-card', () => ({
default: ({ type, pipeline }: { type: string, pipeline: { name: string } }) => (
<div data-testid="template-card" data-type={type}>
{pipeline.name}
@ -18,18 +18,14 @@ vi.mock('@/service/use-pipeline', () => ({
usePipelineTemplateList: (...args: unknown[]) => mockUsePipelineTemplateList(...args),
}))
// ============================================================================
// CustomizedList Component Tests
// ============================================================================
describe('CustomizedList', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Loading State Tests
// --------------------------------------------------------------------------
describe('Loading State', () => {
it('should return null when loading', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -42,9 +38,7 @@ describe('CustomizedList', () => {
})
})
// --------------------------------------------------------------------------
// Empty State Tests
// --------------------------------------------------------------------------
describe('Empty State', () => {
it('should return null when list is empty', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -67,9 +61,7 @@ describe('CustomizedList', () => {
})
})
// --------------------------------------------------------------------------
// Rendering with Data Tests
// --------------------------------------------------------------------------
describe('Rendering with Data', () => {
it('should render title when list has items', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -116,9 +108,7 @@ describe('CustomizedList', () => {
})
})
// --------------------------------------------------------------------------
// API Call Tests
// --------------------------------------------------------------------------
describe('API Call', () => {
it('should call usePipelineTemplateList with type customized', () => {
mockUsePipelineTemplateList.mockReturnValue({
@ -131,9 +121,6 @@ describe('CustomizedList', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have grid layout for cards', () => {
mockUsePipelineTemplateList.mockReturnValue({

View File

@ -1,25 +1,19 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import List from './index'
import List from '../index'
// Mock child components
vi.mock('./built-in-pipeline-list', () => ({
vi.mock('../built-in-pipeline-list', () => ({
default: () => <div data-testid="built-in-list">BuiltInPipelineList</div>,
}))
vi.mock('./customized-list', () => ({
vi.mock('../customized-list', () => ({
default: () => <div data-testid="customized-list">CustomizedList</div>,
}))
// ============================================================================
// List Component Tests
// ============================================================================
describe('List', () => {
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<List />)
@ -37,9 +31,6 @@ describe('List', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container classes', () => {
const { container } = render(<List />)
@ -54,9 +45,7 @@ describe('List', () => {
})
})
// --------------------------------------------------------------------------
// Component Order Tests
// --------------------------------------------------------------------------
describe('Component Order', () => {
it('should render BuiltInPipelineList before CustomizedList', () => {
const { container } = render(<List />)

View File

@ -1,11 +1,9 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Actions from './actions'
import Actions from '../actions'
// ============================================================================
// Actions Component Tests
// ============================================================================
describe('Actions', () => {
const defaultProps = {
@ -21,9 +19,6 @@ describe('Actions', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Actions {...defaultProps} />)
@ -53,9 +48,7 @@ describe('Actions', () => {
})
})
// --------------------------------------------------------------------------
// More Operations Tests
// --------------------------------------------------------------------------
describe('More Operations', () => {
it('should render more operations button when showMoreOperations is true', () => {
const { container } = render(<Actions {...defaultProps} showMoreOperations={true} />)
@ -72,9 +65,6 @@ describe('Actions', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onApplyTemplate when choose button is clicked', () => {
render(<Actions {...defaultProps} />)
@ -95,9 +85,7 @@ describe('Actions', () => {
})
})
// --------------------------------------------------------------------------
// Button Variants Tests
// --------------------------------------------------------------------------
describe('Button Variants', () => {
it('should have primary variant for choose button', () => {
render(<Actions {...defaultProps} />)
@ -112,9 +100,6 @@ describe('Actions', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have absolute positioning', () => {
const { container } = render(<Actions {...defaultProps} />)
@ -141,9 +126,6 @@ describe('Actions', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Actions {...defaultProps} />)

View File

@ -3,11 +3,7 @@ import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import Content from './content'
// ============================================================================
// Test Data Factories
// ============================================================================
import Content from '../content'
const createIconInfo = (overrides: Partial<IconInfo> = {}): IconInfo => ({
icon_type: 'emoji',
@ -25,9 +21,7 @@ const createImageIconInfo = (overrides: Partial<IconInfo> = {}): IconInfo => ({
...overrides,
})
// ============================================================================
// Content Component Tests
// ============================================================================
describe('Content', () => {
const defaultProps = {
@ -37,9 +31,6 @@ describe('Content', () => {
chunkStructure: 'text' as ChunkingMode,
}
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Content {...defaultProps} />)
@ -75,9 +66,7 @@ describe('Content', () => {
})
})
// --------------------------------------------------------------------------
// Icon Rendering Tests
// --------------------------------------------------------------------------
describe('Icon Rendering', () => {
it('should render emoji icon correctly', () => {
const { container } = render(<Content {...defaultProps} />)
@ -104,9 +93,7 @@ describe('Content', () => {
})
})
// --------------------------------------------------------------------------
// Chunk Structure Tests
// --------------------------------------------------------------------------
describe('Chunk Structure', () => {
it('should handle text chunk structure', () => {
render(<Content {...defaultProps} chunkStructure={ChunkingMode.text} />)
@ -132,9 +119,6 @@ describe('Content', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper header layout', () => {
const { container } = render(<Content {...defaultProps} />)
@ -155,9 +139,6 @@ describe('Content', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle empty name', () => {
render(<Content {...defaultProps} name="" />)
@ -186,9 +167,6 @@ describe('Content', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Content {...defaultProps} />)

View File

@ -4,9 +4,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import { ChunkingMode } from '@/models/datasets'
import EditPipelineInfo from './edit-pipeline-info'
import EditPipelineInfo from '../edit-pipeline-info'
// Mock service hooks
const mockUpdatePipeline = vi.fn()
const mockInvalidCustomizedTemplateList = vi.fn()
@ -17,7 +16,6 @@ vi.mock('@/service/use-pipeline', () => ({
useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList,
}))
// Mock Toast
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: vi.fn(),
@ -51,10 +49,6 @@ vi.mock('@/app/components/base/app-icon-picker', () => ({
},
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createPipelineTemplate = (overrides: Partial<PipelineTemplate> = {}): PipelineTemplate => ({
id: 'pipeline-1',
name: 'Test Pipeline',
@ -84,9 +78,7 @@ const createImagePipelineTemplate = (): PipelineTemplate => ({
position: 1,
})
// ============================================================================
// EditPipelineInfo Component Tests
// ============================================================================
describe('EditPipelineInfo', () => {
const defaultProps = {
@ -100,9 +92,6 @@ describe('EditPipelineInfo', () => {
_mockOnClose = undefined
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<EditPipelineInfo {...defaultProps} />)
@ -149,9 +138,6 @@ describe('EditPipelineInfo', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onClose when close button is clicked', () => {
const { container } = render(<EditPipelineInfo {...defaultProps} />)
@ -238,9 +224,6 @@ describe('EditPipelineInfo', () => {
})
})
// --------------------------------------------------------------------------
// Validation Tests
// --------------------------------------------------------------------------
describe('Validation', () => {
it('should show error toast when name is empty', async () => {
render(<EditPipelineInfo {...defaultProps} />)
@ -274,9 +257,7 @@ describe('EditPipelineInfo', () => {
})
})
// --------------------------------------------------------------------------
// Icon Types Tests (Branch Coverage for lines 29-30, 36-37)
// --------------------------------------------------------------------------
describe('Icon Types', () => {
it('should initialize with emoji icon type when pipeline has emoji icon', () => {
const { container } = render(<EditPipelineInfo {...defaultProps} />)
@ -409,7 +390,6 @@ describe('EditPipelineInfo', () => {
const selectEmojiButton = screen.getByTestId('select-emoji')
fireEvent.click(selectEmojiButton)
// Save
const saveButton = screen.getByText(/operation\.save/i)
fireEvent.click(saveButton)
@ -440,7 +420,6 @@ describe('EditPipelineInfo', () => {
const selectImageButton = screen.getByTestId('select-image')
fireEvent.click(selectImageButton)
// Save
const saveButton = screen.getByText(/operation\.save/i)
fireEvent.click(saveButton)
@ -458,9 +437,7 @@ describe('EditPipelineInfo', () => {
})
})
// --------------------------------------------------------------------------
// AppIconPicker Tests (Branch Coverage)
// --------------------------------------------------------------------------
describe('AppIconPicker', () => {
it('should not show picker initially', () => {
render(<EditPipelineInfo {...defaultProps} />)
@ -525,7 +502,6 @@ describe('EditPipelineInfo', () => {
const selectEmojiButton = screen.getByTestId('select-emoji')
fireEvent.click(selectEmojiButton)
// Save
const saveButton = screen.getByText(/operation\.save/i)
fireEvent.click(saveButton)
@ -557,7 +533,6 @@ describe('EditPipelineInfo', () => {
const selectImageButton = screen.getByTestId('select-image')
fireEvent.click(selectImageButton)
// Save
const saveButton = screen.getByText(/operation\.save/i)
fireEvent.click(saveButton)
@ -576,9 +551,7 @@ describe('EditPipelineInfo', () => {
})
})
// --------------------------------------------------------------------------
// Save Request Tests
// --------------------------------------------------------------------------
describe('Save Request', () => {
it('should send correct request with emoji icon', async () => {
mockUpdatePipeline.mockImplementation((_data, callbacks) => {
@ -635,9 +608,6 @@ describe('EditPipelineInfo', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container styling', () => {
const { container } = render(<EditPipelineInfo {...defaultProps} />)
@ -652,9 +622,6 @@ describe('EditPipelineInfo', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<EditPipelineInfo {...defaultProps} />)

View File

@ -3,9 +3,8 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import { ChunkingMode } from '@/models/datasets'
import TemplateCard from './index'
import TemplateCard from '../index'
// Mock next/navigation
const mockPush = vi.fn()
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: mockPush }),
@ -16,7 +15,6 @@ vi.mock('@/app/components/base/amplitude', () => ({
trackEvent: vi.fn(),
}))
// Mock Toast
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: vi.fn(),
@ -61,7 +59,7 @@ let _capturedHandleDelete: (() => void) | undefined
let _capturedHandleExportDSL: (() => void) | undefined
let _capturedOpenEditModal: (() => void) | undefined
vi.mock('./actions', () => ({
vi.mock('../actions', () => ({
default: ({ onApplyTemplate, handleShowTemplateDetails, showMoreOperations, openEditModal, handleExportDSL, handleDelete }: {
onApplyTemplate: () => void
handleShowTemplateDetails: () => void
@ -90,7 +88,7 @@ vi.mock('./actions', () => ({
}))
// Mock EditPipelineInfo component
vi.mock('./edit-pipeline-info', () => ({
vi.mock('../edit-pipeline-info', () => ({
default: ({ onClose }: { onClose: () => void }) => (
<div data-testid="edit-pipeline-info">
<button data-testid="edit-close" onClick={onClose}>Close</button>
@ -99,7 +97,7 @@ vi.mock('./edit-pipeline-info', () => ({
}))
// Mock Details component
vi.mock('./details', () => ({
vi.mock('../details', () => ({
default: ({ onClose, onApplyTemplate }: { onClose: () => void, onApplyTemplate: () => void }) => (
<div data-testid="details-component">
<button data-testid="details-close" onClick={onClose}>Close</button>
@ -108,7 +106,6 @@ vi.mock('./details', () => ({
),
}))
// Mock service hooks
const mockCreateDataset = vi.fn()
const mockInvalidDatasetList = vi.fn()
const mockGetPipelineTemplateInfo = vi.fn()
@ -151,10 +148,6 @@ vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
}),
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createPipelineTemplate = (overrides: Partial<PipelineTemplate> = {}): PipelineTemplate => ({
id: 'pipeline-1',
name: 'Test Pipeline',
@ -170,9 +163,7 @@ const createPipelineTemplate = (overrides: Partial<PipelineTemplate> = {}): Pipe
...overrides,
})
// ============================================================================
// TemplateCard Component Tests
// ============================================================================
describe('TemplateCard', () => {
const defaultProps = {
@ -197,9 +188,6 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<TemplateCard {...defaultProps} />)
@ -230,9 +218,7 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Use Template Flow Tests
// --------------------------------------------------------------------------
describe('Use Template Flow', () => {
it('should show error when template info fetch fails', async () => {
mockGetPipelineTemplateInfo.mockResolvedValue({ data: null })
@ -331,9 +317,7 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Details Modal Tests
// --------------------------------------------------------------------------
describe('Details Modal', () => {
it('should open details modal when details button is clicked', async () => {
render(<TemplateCard {...defaultProps} />)
@ -385,9 +369,7 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Pipeline ID Branch Tests
// --------------------------------------------------------------------------
describe('Pipeline ID Branch', () => {
it('should call handleCheckPluginDependencies when pipeline_id is present', async () => {
mockCreateDataset.mockImplementation((_data, callbacks) => {
@ -437,9 +419,7 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Export DSL Tests (Branch Coverage)
// --------------------------------------------------------------------------
describe('Export DSL', () => {
it('should not export when already exporting', async () => {
mockIsExporting = true
@ -522,9 +502,7 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Delete Flow Tests
// --------------------------------------------------------------------------
describe('Delete Flow', () => {
it('should show confirm dialog when delete is clicked', async () => {
render(<TemplateCard {...defaultProps} />)
@ -620,9 +598,7 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Edit Modal Tests
// --------------------------------------------------------------------------
describe('Edit Modal', () => {
it('should open edit modal when edit button is clicked', async () => {
render(<TemplateCard {...defaultProps} />)
@ -652,9 +628,7 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Props Tests
// --------------------------------------------------------------------------
describe('Props', () => {
it('should show more operations when showMoreOperations is true', () => {
render(<TemplateCard {...defaultProps} showMoreOperations={true} />)
@ -687,9 +661,6 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper card styling', () => {
const { container } = render(<TemplateCard {...defaultProps} />)
@ -710,9 +681,6 @@ describe('TemplateCard', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<TemplateCard {...defaultProps} />)

View File

@ -1,11 +1,9 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Operations from './operations'
import Operations from '../operations'
// ============================================================================
// Operations Component Tests
// ============================================================================
describe('Operations', () => {
const defaultProps = {
@ -18,9 +16,6 @@ describe('Operations', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<Operations {...defaultProps} />)
@ -41,9 +36,6 @@ describe('Operations', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call openEditModal when edit is clicked', () => {
render(<Operations {...defaultProps} />)
@ -106,9 +98,6 @@ describe('Operations', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have divider between sections', () => {
const { container } = render(<Operations {...defaultProps} />)
@ -131,9 +120,6 @@ describe('Operations', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<Operations {...defaultProps} />)

View File

@ -1,12 +1,10 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import ChunkStructureCard from './chunk-structure-card'
import { EffectColor } from './types'
import ChunkStructureCard from '../chunk-structure-card'
import { EffectColor } from '../types'
// ============================================================================
// ChunkStructureCard Component Tests
// ============================================================================
describe('ChunkStructureCard', () => {
const defaultProps = {
@ -16,9 +14,6 @@ describe('ChunkStructureCard', () => {
effectColor: EffectColor.indigo,
}
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<ChunkStructureCard {...defaultProps} />)
@ -53,9 +48,7 @@ describe('ChunkStructureCard', () => {
})
})
// --------------------------------------------------------------------------
// Effect Colors Tests
// --------------------------------------------------------------------------
describe('Effect Colors', () => {
it('should apply indigo effect color', () => {
const { container } = render(
@ -90,9 +83,7 @@ describe('ChunkStructureCard', () => {
})
})
// --------------------------------------------------------------------------
// Icon Background Tests
// --------------------------------------------------------------------------
describe('Icon Background', () => {
it('should apply indigo icon background', () => {
const { container } = render(
@ -119,9 +110,7 @@ describe('ChunkStructureCard', () => {
})
})
// --------------------------------------------------------------------------
// Custom className Tests
// --------------------------------------------------------------------------
describe('Custom className', () => {
it('should apply custom className', () => {
const { container } = render(
@ -140,9 +129,6 @@ describe('ChunkStructureCard', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper card styling', () => {
const { container } = render(<ChunkStructureCard {...defaultProps} />)
@ -169,9 +155,6 @@ describe('ChunkStructureCard', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<ChunkStructureCard {...defaultProps} />)

View File

@ -2,17 +2,13 @@ import { renderHook } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import { useChunkStructureConfig } from './hooks'
import { EffectColor } from './types'
import { useChunkStructureConfig } from '../hooks'
import { EffectColor } from '../types'
// ============================================================================
// useChunkStructureConfig Hook Tests
// ============================================================================
describe('useChunkStructureConfig', () => {
// --------------------------------------------------------------------------
// Return Value Tests
// --------------------------------------------------------------------------
describe('Return Value', () => {
it('should return config object', () => {
const { result } = renderHook(() => useChunkStructureConfig())
@ -36,9 +32,7 @@ describe('useChunkStructureConfig', () => {
})
})
// --------------------------------------------------------------------------
// Text/General Config Tests
// --------------------------------------------------------------------------
describe('Text/General Config', () => {
it('should have title for text mode', () => {
const { result } = renderHook(() => useChunkStructureConfig())
@ -61,9 +55,7 @@ describe('useChunkStructureConfig', () => {
})
})
// --------------------------------------------------------------------------
// Parent-Child Config Tests
// --------------------------------------------------------------------------
describe('Parent-Child Config', () => {
it('should have title for parent-child mode', () => {
const { result } = renderHook(() => useChunkStructureConfig())
@ -86,9 +78,7 @@ describe('useChunkStructureConfig', () => {
})
})
// --------------------------------------------------------------------------
// Q&A Config Tests
// --------------------------------------------------------------------------
describe('Q&A Config', () => {
it('should have title for qa mode', () => {
const { result } = renderHook(() => useChunkStructureConfig())
@ -111,9 +101,7 @@ describe('useChunkStructureConfig', () => {
})
})
// --------------------------------------------------------------------------
// Option Structure Tests
// --------------------------------------------------------------------------
describe('Option Structure', () => {
it('should have all required fields in each option', () => {
const { result } = renderHook(() => useChunkStructureConfig())

View File

@ -1,7 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Details from './index'
import Details from '../index'
// Mock WorkflowPreview
vi.mock('@/app/components/workflow/workflow-preview', () => ({
@ -12,16 +12,11 @@ vi.mock('@/app/components/workflow/workflow-preview', () => ({
),
}))
// Mock service hook
const mockUsePipelineTemplateById = vi.fn()
vi.mock('@/service/use-pipeline', () => ({
usePipelineTemplateById: (...args: unknown[]) => mockUsePipelineTemplateById(...args),
}))
// ============================================================================
// Test Data Factories
// ============================================================================
const createPipelineTemplateInfo = (overrides = {}) => ({
name: 'Test Pipeline',
description: 'This is a test pipeline',
@ -52,9 +47,7 @@ const createImageIconPipelineInfo = () => ({
},
})
// ============================================================================
// Details Component Tests
// ============================================================================
describe('Details', () => {
const defaultProps = {
@ -68,9 +61,7 @@ describe('Details', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Loading State Tests
// --------------------------------------------------------------------------
describe('Loading State', () => {
it('should show loading when data is not available', () => {
mockUsePipelineTemplateById.mockReturnValue({
@ -83,9 +74,6 @@ describe('Details', () => {
})
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing when data is available', () => {
mockUsePipelineTemplateById.mockReturnValue({
@ -180,9 +168,6 @@ describe('Details', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onClose when close button is clicked', () => {
mockUsePipelineTemplateById.mockReturnValue({
@ -209,9 +194,7 @@ describe('Details', () => {
})
})
// --------------------------------------------------------------------------
// Icon Types Tests
// --------------------------------------------------------------------------
describe('Icon Types', () => {
it('should handle emoji icon type', () => {
mockUsePipelineTemplateById.mockReturnValue({
@ -245,9 +228,7 @@ describe('Details', () => {
})
})
// --------------------------------------------------------------------------
// API Call Tests
// --------------------------------------------------------------------------
describe('API Call', () => {
it('should call usePipelineTemplateById with correct params', () => {
mockUsePipelineTemplateById.mockReturnValue({
@ -276,9 +257,7 @@ describe('Details', () => {
})
})
// --------------------------------------------------------------------------
// Chunk Structure Tests
// --------------------------------------------------------------------------
describe('Chunk Structure', () => {
it('should render chunk structure card for text mode', () => {
mockUsePipelineTemplateById.mockReturnValue({
@ -308,9 +287,6 @@ describe('Details', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have proper container styling', () => {
mockUsePipelineTemplateById.mockReturnValue({
@ -343,9 +319,6 @@ describe('Details', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
mockUsePipelineTemplateById.mockReturnValue({

View File

@ -1,18 +1,14 @@
/* eslint-disable next/no-img-element */
import type { ReactNode } from 'react'
import type { IndexingStatusResponse } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DataSourceType } from '@/models/datasets'
import IndexingProgressItem from './indexing-progress-item'
import IndexingProgressItem from '../indexing-progress-item'
vi.mock('next/image', () => ({
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => <img {...props} />,
}))
vi.mock('@/app/components/billing/priority-label', () => ({
default: () => <span data-testid="priority-label">Priority</span>,
}))
vi.mock('../../common/document-file-icon', () => ({
vi.mock('../../../common/document-file-icon', () => ({
default: ({ name }: { name?: string }) => <span data-testid="file-icon">{name}</span>,
}))
vi.mock('@/app/components/base/notion-icon', () => ({
@ -93,7 +89,7 @@ describe('IndexingProgressItem', () => {
)
// No progress percentage should be shown for completed
expect(screen.queryByText('%')).not.toBeInTheDocument()
expect(screen.queryByText(/%/)).not.toBeInTheDocument()
})
it('should render error icon with tooltip for error status', () => {

View File

@ -1,19 +1,10 @@
/* eslint-disable next/no-img-element */
import type { ProcessRuleResponse } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ProcessMode } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import RuleDetail from './rule-detail'
import RuleDetail from '../rule-detail'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, opts?: Record<string, unknown>) => `${opts?.ns ? `${opts.ns}.` : ''}${key}`,
}),
}))
vi.mock('next/image', () => ({
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => <img {...props} />,
}))
vi.mock('@/app/components/datasets/documents/detail/metadata', () => ({
FieldInfo: ({ label, displayedValue }: { label: string, displayedValue: string }) => (
<div data-testid="field-info">
@ -22,7 +13,7 @@ vi.mock('@/app/components/datasets/documents/detail/metadata', () => ({
</div>
),
}))
vi.mock('../icons', () => ({
vi.mock('../../icons', () => ({
indexMethodIcon: { economical: '/icons/economical.svg', high_quality: '/icons/hq.svg' },
retrievalIcon: { fullText: '/icons/ft.svg', hybrid: '/icons/hy.svg', vector: '/icons/vec.svg' },
}))

View File

@ -1,12 +1,7 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import UpgradeBanner from './upgrade-banner'
import UpgradeBanner from '../upgrade-banner'
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
vi.mock('@/app/components/base/icons/src/vender/solid/general', () => ({
ZapFast: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="zap-icon" {...props} />,
}))
@ -23,7 +18,7 @@ describe('UpgradeBanner', () => {
render(<UpgradeBanner />)
expect(screen.getByTestId('zap-icon')).toBeInTheDocument()
expect(screen.getByText('plansCommon.documentProcessingPriorityUpgrade')).toBeInTheDocument()
expect(screen.getByText('billing.plansCommon.documentProcessingPriorityUpgrade')).toBeInTheDocument()
expect(screen.getByTestId('upgrade-btn')).toBeInTheDocument()
})

View File

@ -1,6 +1,6 @@
import { act, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useIndexingStatusPolling } from './use-indexing-status-polling'
import { useIndexingStatusPolling } from '../use-indexing-status-polling'
const mockFetchIndexingStatusBatch = vi.fn()

View File

@ -1,6 +1,6 @@
import type { DataSourceInfo, FullDocumentDetail, IndexingStatusResponse } from '@/models/datasets'
import { describe, expect, it } from 'vitest'
import { createDocumentLookup, getFileType, getSourcePercent, isLegacyDataSourceInfo, isSourceEmbedding } from './utils'
import { createDocumentLookup, getFileType, getSourcePercent, isLegacyDataSourceInfo, isSourceEmbedding } from '../utils'
describe('isLegacyDataSourceInfo', () => {
it('should return true when upload_file object exists', () => {

View File

@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import * as React from 'react'
import { createEmptyDataset } from '@/service/datasets'
import { useInvalidDatasetList } from '@/service/knowledge/use-dataset'
import EmptyDatasetCreationModal from './index'
import EmptyDatasetCreationModal from '../index'
// Mock Next.js router
const mockPush = vi.fn()
@ -54,15 +54,11 @@ describe('EmptyDatasetCreationModal', () => {
} as ReturnType<typeof createEmptyDataset> extends Promise<infer T> ? T : never)
})
// ==========================================
// Rendering Tests - Verify component renders correctly
// ==========================================
describe('Rendering', () => {
it('should render without crashing when show is true', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<EmptyDatasetCreationModal {...props} />)
// Assert - Check modal title is rendered
@ -70,13 +66,10 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should render modal with correct elements', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<EmptyDatasetCreationModal {...props} />)
// Assert
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepOne.modal.tip')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepOne.modal.input')).toBeInTheDocument()
@ -86,22 +79,17 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should render input with empty value initially', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<EmptyDatasetCreationModal {...props} />)
// Assert
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
expect(input.value).toBe('')
})
it('should not render modal content when show is false', () => {
// Arrange
const props = createDefaultProps({ show: false })
// Act
render(<EmptyDatasetCreationModal {...props} />)
// Assert - Modal should not be visible (check for absence of title)
@ -109,29 +97,22 @@ describe('EmptyDatasetCreationModal', () => {
})
})
// ==========================================
// Props Testing - Verify all prop variations work correctly
// ==========================================
describe('Props', () => {
describe('show prop', () => {
it('should show modal when show is true', () => {
// Arrange & Act
render(<EmptyDatasetCreationModal show={true} onHide={vi.fn()} />)
// Assert
expect(screen.getByText('datasetCreation.stepOne.modal.title')).toBeInTheDocument()
})
it('should hide modal when show is false', () => {
// Arrange & Act
render(<EmptyDatasetCreationModal show={false} onHide={vi.fn()} />)
// Assert
expect(screen.queryByText('datasetCreation.stepOne.modal.title')).not.toBeInTheDocument()
})
it('should toggle visibility when show prop changes', () => {
// Arrange
const onHide = vi.fn()
const { rerender } = render(<EmptyDatasetCreationModal show={false} onHide={onHide} />)
@ -146,20 +127,16 @@ describe('EmptyDatasetCreationModal', () => {
describe('onHide prop', () => {
it('should call onHide when cancel button is clicked', () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
// Act
const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton')
fireEvent.click(cancelButton)
// Assert
expect(mockOnHide).toHaveBeenCalledTimes(1)
})
it('should call onHide when close icon is clicked', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
@ -172,31 +149,24 @@ describe('EmptyDatasetCreationModal', () => {
expect(closeButton).toBeInTheDocument()
fireEvent.click(closeButton!)
// Assert
expect(mockOnHide).toHaveBeenCalledTimes(1)
})
})
})
// ==========================================
// State Management - Test input state updates
// ==========================================
describe('State Management', () => {
it('should update input value when user types', () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
// Act
fireEvent.change(input, { target: { value: 'My Dataset' } })
// Assert
expect(input.value).toBe('My Dataset')
})
it('should persist input value when modal is hidden and shown again via rerender', () => {
// Arrange
const onHide = vi.fn()
const { rerender } = render(<EmptyDatasetCreationModal show={true} onHide={onHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
@ -215,12 +185,10 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should handle consecutive input changes', () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder') as HTMLInputElement
// Act & Assert
fireEvent.change(input, { target: { value: 'A' } })
expect(input.value).toBe('A')
@ -232,29 +200,23 @@ describe('EmptyDatasetCreationModal', () => {
})
})
// ==========================================
// User Interactions - Test event handlers
// ==========================================
describe('User Interactions', () => {
it('should submit form when confirm button is clicked with valid input', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Valid Dataset Name' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Valid Dataset Name' })
})
})
it('should show error notification when input is empty', async () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
@ -262,7 +224,6 @@ describe('EmptyDatasetCreationModal', () => {
// Act - Click confirm without entering a name
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
@ -273,7 +234,6 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should show error notification when input exceeds 40 characters', async () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
@ -284,7 +244,6 @@ describe('EmptyDatasetCreationModal', () => {
fireEvent.change(input, { target: { value: longName } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
@ -295,7 +254,6 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should allow exactly 40 characters', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
@ -306,94 +264,76 @@ describe('EmptyDatasetCreationModal', () => {
fireEvent.change(input, { target: { value: exactLengthName } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: exactLengthName })
})
})
it('should close modal on cancel button click', () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const cancelButton = screen.getByText('datasetCreation.stepOne.modal.cancelButton')
// Act
fireEvent.click(cancelButton)
// Assert
expect(mockOnHide).toHaveBeenCalledTimes(1)
})
})
// ==========================================
// API Calls - Test API interactions
// ==========================================
describe('API Calls', () => {
it('should call createEmptyDataset with correct parameters', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'New Dataset' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'New Dataset' })
})
})
it('should call invalidDatasetList after successful creation', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test Dataset' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockInvalidDatasetList).toHaveBeenCalled()
})
})
it('should call onHide after successful creation', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test Dataset' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockOnHide).toHaveBeenCalled()
})
})
it('should show error notification on API failure', async () => {
// Arrange
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test Dataset' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
@ -403,14 +343,12 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should not call onHide on API failure', async () => {
// Arrange
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test Dataset' } })
fireEvent.click(confirmButton)
@ -423,18 +361,15 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should not invalidate dataset list on API failure', async () => {
// Arrange
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test Dataset' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalled()
})
@ -442,12 +377,9 @@ describe('EmptyDatasetCreationModal', () => {
})
})
// ==========================================
// Router Navigation - Test Next.js router
// ==========================================
describe('Router Navigation', () => {
it('should navigate to dataset documents page after successful creation', async () => {
// Arrange
mockCreateEmptyDataset.mockResolvedValue({
id: 'test-dataset-456',
name: 'Test',
@ -457,18 +389,15 @@ describe('EmptyDatasetCreationModal', () => {
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-456/documents')
})
})
it('should not navigate on validation error', async () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
@ -476,7 +405,6 @@ describe('EmptyDatasetCreationModal', () => {
// Act - Click confirm with empty input
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalled()
})
@ -484,18 +412,15 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should not navigate on API error', async () => {
// Arrange
mockCreateEmptyDataset.mockRejectedValue(new Error('API Error'))
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalled()
})
@ -503,12 +428,9 @@ describe('EmptyDatasetCreationModal', () => {
})
})
// ==========================================
// Edge Cases - Test boundary conditions and error handling
// ==========================================
describe('Edge Cases', () => {
it('should handle whitespace-only input as valid (component behavior)', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
@ -525,41 +447,34 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should handle special characters in input', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Test @#$% Dataset!' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: 'Test @#$% Dataset!' })
})
})
it('should handle Unicode characters in input', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: '数据集测试 🚀' } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: '数据集测试 🚀' })
})
})
it('should handle input at exactly 40 character boundary', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
@ -570,14 +485,12 @@ describe('EmptyDatasetCreationModal', () => {
fireEvent.change(input, { target: { value: name40Chars } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockCreateEmptyDataset).toHaveBeenCalledWith({ name: name40Chars })
})
})
it('should reject input at 41 character boundary', async () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
@ -588,7 +501,6 @@ describe('EmptyDatasetCreationModal', () => {
fireEvent.change(input, { target: { value: name41Chars } })
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
@ -599,7 +511,6 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should handle rapid consecutive submits', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
@ -618,13 +529,11 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should handle input with leading/trailing spaces', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: ' Dataset Name ' } })
fireEvent.click(confirmButton)
@ -635,13 +544,11 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should handle newline characters in input (browser strips newlines)', async () => {
// Arrange
const mockOnHide = vi.fn()
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Line1\nLine2' } })
fireEvent.click(confirmButton)
@ -652,20 +559,15 @@ describe('EmptyDatasetCreationModal', () => {
})
})
// ==========================================
// Validation Tests - Test input validation
// ==========================================
describe('Validation', () => {
it('should not submit when input is empty string', async () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.click(confirmButton)
// Assert
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith({
type: 'error',
@ -675,13 +577,11 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should validate length before calling API', async () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'A'.repeat(50) } })
fireEvent.click(confirmButton)
@ -696,7 +596,6 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should validate empty string before length check', async () => {
// Arrange
const props = createDefaultProps()
render(<EmptyDatasetCreationModal {...props} />)
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
@ -714,12 +613,9 @@ describe('EmptyDatasetCreationModal', () => {
})
})
// ==========================================
// Integration Tests - Test complete flows
// ==========================================
describe('Integration', () => {
it('should complete full successful creation flow', async () => {
// Arrange
const mockOnHide = vi.fn()
mockCreateEmptyDataset.mockResolvedValue({
id: 'new-id-789',
@ -729,7 +625,6 @@ describe('EmptyDatasetCreationModal', () => {
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Complete Flow Test' } })
fireEvent.click(confirmButton)
@ -747,14 +642,12 @@ describe('EmptyDatasetCreationModal', () => {
})
it('should handle error flow correctly', async () => {
// Arrange
const mockOnHide = vi.fn()
mockCreateEmptyDataset.mockRejectedValue(new Error('Server Error'))
render(<EmptyDatasetCreationModal show={true} onHide={mockOnHide} />)
const input = screen.getByPlaceholderText('datasetCreation.stepOne.modal.placeholder')
const confirmButton = screen.getByText('datasetCreation.stepOne.modal.confirmButton')
// Act
fireEvent.change(input, { target: { value: 'Error Test' } })
fireEvent.click(confirmButton)

View File

@ -2,7 +2,7 @@ import type { MockedFunction } from 'vitest'
import type { CustomFile as File } from '@/models/datasets'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fetchFilePreview } from '@/service/common'
import FilePreview from './index'
import FilePreview from '../index'
// Mock the fetchFilePreview service
vi.mock('@/service/common', () => ({
@ -48,9 +48,7 @@ const findLoadingSpinner = (container: HTMLElement) => {
return container.querySelector('.spin-animation')
}
// ============================================================================
// FilePreview Component Tests
// ============================================================================
describe('FilePreview', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -58,33 +56,25 @@ describe('FilePreview', () => {
mockFetchFilePreview.mockResolvedValue({ content: 'Preview content here' })
})
// --------------------------------------------------------------------------
// Rendering Tests - Verify component renders properly
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', async () => {
// Arrange & Act
renderFilePreview()
// Assert
await waitFor(() => {
expect(screen.getByText('datasetCreation.stepOne.filePreview')).toBeInTheDocument()
})
})
it('should render file preview header', async () => {
// Arrange & Act
renderFilePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.filePreview')).toBeInTheDocument()
})
it('should render close button with XMarkIcon', async () => {
// Arrange & Act
const { container } = renderFilePreview()
// Assert
const closeButton = container.querySelector('.cursor-pointer')
expect(closeButton).toBeInTheDocument()
const xMarkIcon = closeButton?.querySelector('svg')
@ -92,42 +82,32 @@ describe('FilePreview', () => {
})
it('should render file name without extension', async () => {
// Arrange
const file = createMockFile({ name: 'document.pdf' })
// Act
renderFilePreview({ file })
// Assert
await waitFor(() => {
expect(screen.getByText('document')).toBeInTheDocument()
})
})
it('should render file extension', async () => {
// Arrange
const file = createMockFile({ extension: 'pdf' })
// Act
renderFilePreview({ file })
// Assert
expect(screen.getByText('.pdf')).toBeInTheDocument()
})
it('should apply correct CSS classes to container', async () => {
// Arrange & Act
const { container } = renderFilePreview()
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('h-full')
})
})
// --------------------------------------------------------------------------
// Loading State Tests
// --------------------------------------------------------------------------
describe('Loading State', () => {
it('should show loading indicator initially', async () => {
// Arrange - Delay API response to keep loading state
@ -135,7 +115,6 @@ describe('FilePreview', () => {
() => new Promise(resolve => setTimeout(() => resolve({ content: 'test' }), 100)),
)
// Act
const { container } = renderFilePreview()
// Assert - Loading should be visible initially (using spin-animation class)
@ -144,13 +123,10 @@ describe('FilePreview', () => {
})
it('should hide loading indicator after content loads', async () => {
// Arrange
mockFetchFilePreview.mockResolvedValue({ content: 'Loaded content' })
// Act
const { container } = renderFilePreview()
// Assert
await waitFor(() => {
expect(screen.getByText('Loaded content')).toBeInTheDocument()
})
@ -160,7 +136,6 @@ describe('FilePreview', () => {
})
it('should show loading when file changes', async () => {
// Arrange
const file1 = createMockFile({ id: 'file-1', name: 'file1.txt' })
const file2 = createMockFile({ id: 'file-2', name: 'file2.txt' })
@ -207,48 +182,36 @@ describe('FilePreview', () => {
})
})
// --------------------------------------------------------------------------
// API Call Tests
// --------------------------------------------------------------------------
describe('API Calls', () => {
it('should call fetchFilePreview with correct fileID', async () => {
// Arrange
const file = createMockFile({ id: 'test-file-id' })
// Act
renderFilePreview({ file })
// Assert
await waitFor(() => {
expect(mockFetchFilePreview).toHaveBeenCalledWith({ fileID: 'test-file-id' })
})
})
it('should not call fetchFilePreview when file is undefined', async () => {
// Arrange & Act
renderFilePreview({ file: undefined })
// Assert
expect(mockFetchFilePreview).not.toHaveBeenCalled()
})
it('should not call fetchFilePreview when file has no id', async () => {
// Arrange
const file = createMockFile({ id: undefined })
// Act
renderFilePreview({ file })
// Assert
expect(mockFetchFilePreview).not.toHaveBeenCalled()
})
it('should call fetchFilePreview again when file changes', async () => {
// Arrange
const file1 = createMockFile({ id: 'file-1' })
const file2 = createMockFile({ id: 'file-2' })
// Act
const { rerender } = render(
<FilePreview file={file1} hidePreview={vi.fn()} />,
)
@ -259,7 +222,6 @@ describe('FilePreview', () => {
rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)
// Assert
await waitFor(() => {
expect(mockFetchFilePreview).toHaveBeenCalledWith({ fileID: 'file-2' })
expect(mockFetchFilePreview).toHaveBeenCalledTimes(2)
@ -267,23 +229,18 @@ describe('FilePreview', () => {
})
it('should handle API success and display content', async () => {
// Arrange
mockFetchFilePreview.mockResolvedValue({ content: 'File preview content from API' })
// Act
renderFilePreview()
// Assert
await waitFor(() => {
expect(screen.getByText('File preview content from API')).toBeInTheDocument()
})
})
it('should handle API error gracefully', async () => {
// Arrange
mockFetchFilePreview.mockRejectedValue(new Error('Network error'))
// Act
const { container } = renderFilePreview()
// Assert - Component should not crash, loading may persist
@ -295,10 +252,8 @@ describe('FilePreview', () => {
})
it('should handle empty content response', async () => {
// Arrange
mockFetchFilePreview.mockResolvedValue({ content: '' })
// Act
const { container } = renderFilePreview()
// Assert - Should still render without loading
@ -309,29 +264,21 @@ describe('FilePreview', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call hidePreview when close button is clicked', async () => {
// Arrange
const hidePreview = vi.fn()
const { container } = renderFilePreview({ hidePreview })
// Act
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
// Assert
expect(hidePreview).toHaveBeenCalledTimes(1)
})
it('should call hidePreview with event object when clicked', async () => {
// Arrange
const hidePreview = vi.fn()
const { container } = renderFilePreview({ hidePreview })
// Act
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
@ -341,52 +288,40 @@ describe('FilePreview', () => {
})
it('should handle multiple clicks on close button', async () => {
// Arrange
const hidePreview = vi.fn()
const { container } = renderFilePreview({ hidePreview })
// Act
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
fireEvent.click(closeButton)
fireEvent.click(closeButton)
// Assert
expect(hidePreview).toHaveBeenCalledTimes(3)
})
})
// --------------------------------------------------------------------------
// State Management Tests
// --------------------------------------------------------------------------
describe('State Management', () => {
it('should initialize with loading state true', async () => {
// Arrange - Keep loading indefinitely (never resolves)
mockFetchFilePreview.mockImplementation(() => new Promise(() => { /* intentionally empty */ }))
// Act
const { container } = renderFilePreview()
// Assert
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).toBeInTheDocument()
})
it('should update previewContent state after successful fetch', async () => {
// Arrange
mockFetchFilePreview.mockResolvedValue({ content: 'New preview content' })
// Act
renderFilePreview()
// Assert
await waitFor(() => {
expect(screen.getByText('New preview content')).toBeInTheDocument()
})
})
it('should reset loading to true when file changes', async () => {
// Arrange
const file1 = createMockFile({ id: 'file-1' })
const file2 = createMockFile({ id: 'file-2' })
@ -394,7 +329,6 @@ describe('FilePreview', () => {
.mockResolvedValueOnce({ content: 'Content 1' })
.mockImplementationOnce(() => new Promise(() => { /* never resolves */ }))
// Act
const { rerender, container } = render(
<FilePreview file={file1} hidePreview={vi.fn()} />,
)
@ -414,7 +348,6 @@ describe('FilePreview', () => {
})
it('should preserve content until new content loads', async () => {
// Arrange
const file1 = createMockFile({ id: 'file-1' })
const file2 = createMockFile({ id: 'file-2' })
@ -424,7 +357,6 @@ describe('FilePreview', () => {
.mockResolvedValueOnce({ content: 'Content 1' })
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
// Act
const { rerender } = render(
<FilePreview file={file1} hidePreview={vi.fn()} />,
)
@ -448,25 +380,18 @@ describe('FilePreview', () => {
})
})
// --------------------------------------------------------------------------
// Props Testing
// --------------------------------------------------------------------------
describe('Props', () => {
describe('file prop', () => {
it('should render correctly with file prop', async () => {
// Arrange
const file = createMockFile({ name: 'my-document.pdf', extension: 'pdf' })
// Act
renderFilePreview({ file })
// Assert
expect(screen.getByText('my-document')).toBeInTheDocument()
expect(screen.getByText('.pdf')).toBeInTheDocument()
})
it('should render correctly without file prop', async () => {
// Arrange & Act
renderFilePreview({ file: undefined })
// Assert - Header should still render
@ -474,10 +399,8 @@ describe('FilePreview', () => {
})
it('should handle file with multiple dots in name', async () => {
// Arrange
const file = createMockFile({ name: 'my.document.v2.pdf' })
// Act
renderFilePreview({ file })
// Assert - Should join all parts except last with comma
@ -485,10 +408,8 @@ describe('FilePreview', () => {
})
it('should handle file with no extension in name', async () => {
// Arrange
const file = createMockFile({ name: 'README' })
// Act
const { container } = renderFilePreview({ file })
// Assert - getFileName returns empty for single segment, but component still renders
@ -500,10 +421,8 @@ describe('FilePreview', () => {
})
it('should handle file with empty name', async () => {
// Arrange
const file = createMockFile({ name: '' })
// Act
const { container } = renderFilePreview({ file })
// Assert - Should not crash
@ -513,10 +432,8 @@ describe('FilePreview', () => {
describe('hidePreview prop', () => {
it('should accept hidePreview callback', async () => {
// Arrange
const hidePreview = vi.fn()
// Act
renderFilePreview({ hidePreview })
// Assert - No errors thrown
@ -525,15 +442,10 @@ describe('FilePreview', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle file with undefined id', async () => {
// Arrange
const file = createMockFile({ id: undefined })
// Act
const { container } = renderFilePreview({ file })
// Assert - Should not call API, remain in loading state
@ -542,10 +454,8 @@ describe('FilePreview', () => {
})
it('should handle file with empty string id', async () => {
// Arrange
const file = createMockFile({ id: '' })
// Act
renderFilePreview({ file })
// Assert - Empty string is falsy, should not call API
@ -553,48 +463,37 @@ describe('FilePreview', () => {
})
it('should handle very long file names', async () => {
// Arrange
const longName = `${'a'.repeat(200)}.pdf`
const file = createMockFile({ name: longName })
// Act
renderFilePreview({ file })
// Assert
expect(screen.getByText('a'.repeat(200))).toBeInTheDocument()
})
it('should handle file with special characters in name', async () => {
// Arrange
const file = createMockFile({ name: 'file-with_special@#$%.txt' })
// Act
renderFilePreview({ file })
// Assert
expect(screen.getByText('file-with_special@#$%')).toBeInTheDocument()
})
it('should handle very long preview content', async () => {
// Arrange
const longContent = 'x'.repeat(10000)
mockFetchFilePreview.mockResolvedValue({ content: longContent })
// Act
renderFilePreview()
// Assert
await waitFor(() => {
expect(screen.getByText(longContent)).toBeInTheDocument()
})
})
it('should handle preview content with special characters safely', async () => {
// Arrange
const specialContent = '<script>alert("xss")</script>\n\t& < > "'
mockFetchFilePreview.mockResolvedValue({ content: specialContent })
// Act
const { container } = renderFilePreview()
// Assert - Should render as text, not execute scripts
@ -607,25 +506,20 @@ describe('FilePreview', () => {
})
it('should handle preview content with unicode', async () => {
// Arrange
const unicodeContent = '中文内容 🚀 émojis & spëcîal çhàrs'
mockFetchFilePreview.mockResolvedValue({ content: unicodeContent })
// Act
renderFilePreview()
// Assert
await waitFor(() => {
expect(screen.getByText(unicodeContent)).toBeInTheDocument()
})
})
it('should handle preview content with newlines', async () => {
// Arrange
const multilineContent = 'Line 1\nLine 2\nLine 3'
mockFetchFilePreview.mockResolvedValue({ content: multilineContent })
// Act
const { container } = renderFilePreview()
// Assert - Content should be in the DOM
@ -639,10 +533,8 @@ describe('FilePreview', () => {
})
it('should handle null content from API', async () => {
// Arrange
mockFetchFilePreview.mockResolvedValue({ content: null as unknown as string })
// Act
const { container } = renderFilePreview()
// Assert - Should not crash
@ -652,16 +544,12 @@ describe('FilePreview', () => {
})
})
// --------------------------------------------------------------------------
// Side Effects and Cleanup Tests
// --------------------------------------------------------------------------
describe('Side Effects and Cleanup', () => {
it('should trigger effect when file prop changes', async () => {
// Arrange
const file1 = createMockFile({ id: 'file-1' })
const file2 = createMockFile({ id: 'file-2' })
// Act
const { rerender } = render(
<FilePreview file={file1} hidePreview={vi.fn()} />,
)
@ -672,19 +560,16 @@ describe('FilePreview', () => {
rerender(<FilePreview file={file2} hidePreview={vi.fn()} />)
// Assert
await waitFor(() => {
expect(mockFetchFilePreview).toHaveBeenCalledTimes(2)
})
})
it('should not trigger effect when hidePreview changes', async () => {
// Arrange
const file = createMockFile()
const hidePreview1 = vi.fn()
const hidePreview2 = vi.fn()
// Act
const { rerender } = render(
<FilePreview file={file} hidePreview={hidePreview1} />,
)
@ -703,11 +588,9 @@ describe('FilePreview', () => {
})
it('should handle rapid file changes', async () => {
// Arrange
const files = Array.from({ length: 5 }, (_, i) =>
createMockFile({ id: `file-${i}` }))
// Act
const { rerender } = render(
<FilePreview file={files[0]} hidePreview={vi.fn()} />,
)
@ -723,12 +606,10 @@ describe('FilePreview', () => {
})
it('should handle unmount during loading', async () => {
// Arrange
mockFetchFilePreview.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
)
// Act
const { unmount } = renderFilePreview()
// Unmount before API resolves
@ -739,10 +620,8 @@ describe('FilePreview', () => {
})
it('should handle file changing from defined to undefined', async () => {
// Arrange
const file = createMockFile()
// Act
const { rerender, container } = render(
<FilePreview file={file} hidePreview={vi.fn()} />,
)
@ -759,26 +638,19 @@ describe('FilePreview', () => {
})
})
// --------------------------------------------------------------------------
// getFileName Helper Tests
// --------------------------------------------------------------------------
describe('getFileName Helper', () => {
it('should extract name without extension for simple filename', async () => {
// Arrange
const file = createMockFile({ name: 'document.pdf' })
// Act
renderFilePreview({ file })
// Assert
expect(screen.getByText('document')).toBeInTheDocument()
})
it('should handle filename with multiple dots', async () => {
// Arrange
const file = createMockFile({ name: 'file.name.with.dots.txt' })
// Act
renderFilePreview({ file })
// Assert - Should join all parts except last with comma
@ -786,10 +658,8 @@ describe('FilePreview', () => {
})
it('should return empty for filename without dot', async () => {
// Arrange
const file = createMockFile({ name: 'nodotfile' })
// Act
const { container } = renderFilePreview({ file })
// Assert - slice(0, -1) on single element array returns empty
@ -799,7 +669,6 @@ describe('FilePreview', () => {
})
it('should return empty string when file is undefined', async () => {
// Arrange & Act
const { container } = renderFilePreview({ file: undefined })
// Assert - File name area should have empty first span
@ -808,38 +677,27 @@ describe('FilePreview', () => {
})
})
// --------------------------------------------------------------------------
// Accessibility Tests
// --------------------------------------------------------------------------
describe('Accessibility', () => {
it('should have clickable close button with visual indicator', async () => {
// Arrange & Act
const { container } = renderFilePreview()
// Assert
const closeButton = container.querySelector('.cursor-pointer')
expect(closeButton).toBeInTheDocument()
expect(closeButton).toHaveClass('cursor-pointer')
})
it('should have proper heading structure', async () => {
// Arrange & Act
renderFilePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.filePreview')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Error Handling Tests
// --------------------------------------------------------------------------
describe('Error Handling', () => {
it('should not crash on API network error', async () => {
// Arrange
mockFetchFilePreview.mockRejectedValue(new Error('Network Error'))
// Act
const { container } = renderFilePreview()
// Assert - Component should still render
@ -849,26 +707,20 @@ describe('FilePreview', () => {
})
it('should not crash on API timeout', async () => {
// Arrange
mockFetchFilePreview.mockRejectedValue(new Error('Timeout'))
// Act
const { container } = renderFilePreview()
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should not crash on malformed API response', async () => {
// Arrange
mockFetchFilePreview.mockResolvedValue({} as { content: string })
// Act
const { container } = renderFilePreview()
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})

View File

@ -1,26 +1,9 @@
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PROGRESS_NOT_STARTED } from './constants'
import FileUploader from './index'
import { PROGRESS_NOT_STARTED } from '../constants'
import FileUploader from '../index'
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
'stepOne.uploader.title': 'Upload Files',
'stepOne.uploader.button': 'Drag and drop files, or',
'stepOne.uploader.buttonSingleFile': 'Drag and drop file, or',
'stepOne.uploader.browse': 'Browse',
'stepOne.uploader.tip': 'Supports various file types',
}
return translations[key] || key
},
}),
}))
// Mock ToastContext
const mockNotify = vi.fn()
vi.mock('use-context-selector', async () => {
const actual = await vi.importActual<typeof import('use-context-selector')>('use-context-selector')
@ -118,22 +101,22 @@ describe('FileUploader', () => {
describe('rendering', () => {
it('should render the component', () => {
render(<FileUploader {...defaultProps} />)
expect(screen.getByText('Upload Files')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepOne.uploader.title')).toBeInTheDocument()
})
it('should render dropzone when no files', () => {
render(<FileUploader {...defaultProps} />)
expect(screen.getByText(/Drag and drop files/i)).toBeInTheDocument()
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.button/)).toBeInTheDocument()
})
it('should render browse button', () => {
render(<FileUploader {...defaultProps} />)
expect(screen.getByText('Browse')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepOne.uploader.browse')).toBeInTheDocument()
})
it('should apply custom title className', () => {
render(<FileUploader {...defaultProps} titleClassName="custom-class" />)
const title = screen.getByText('Upload Files')
const title = screen.getByText('datasetCreation.stepOne.uploader.title')
expect(title).toHaveClass('custom-class')
})
})
@ -162,19 +145,19 @@ describe('FileUploader', () => {
describe('batch upload mode', () => {
it('should show dropzone with batch upload enabled', () => {
render(<FileUploader {...defaultProps} supportBatchUpload={true} />)
expect(screen.getByText(/Drag and drop files/i)).toBeInTheDocument()
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.button/)).toBeInTheDocument()
})
it('should show single file text when batch upload disabled', () => {
render(<FileUploader {...defaultProps} supportBatchUpload={false} />)
expect(screen.getByText(/Drag and drop file/i)).toBeInTheDocument()
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.buttonSingleFile/)).toBeInTheDocument()
})
it('should hide dropzone when not batch upload and has files', () => {
const fileList = [createMockFileItem()]
render(<FileUploader {...defaultProps} supportBatchUpload={false} fileList={fileList} />)
expect(screen.queryByText(/Drag and drop/i)).not.toBeInTheDocument()
expect(screen.queryByText(/datasetCreation\.stepOne\.uploader\.button/)).not.toBeInTheDocument()
})
})
@ -217,7 +200,7 @@ describe('FileUploader', () => {
render(<FileUploader {...defaultProps} />)
// The browse label should trigger file input click
const browseLabel = screen.getByText('Browse')
const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse')
expect(browseLabel).toHaveClass('cursor-pointer')
})
})

View File

@ -1,9 +1,9 @@
import type { FileListItemProps } from './file-list-item'
import type { FileListItemProps } from '../file-list-item'
import type { CustomFile as File, FileItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../constants'
import FileListItem from './file-list-item'
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../../constants'
import FileListItem from '../file-list-item'
// Mock theme hook - can be changed per test
let mockTheme = 'light'

View File

@ -1,33 +1,12 @@
import type { RefObject } from 'react'
import type { UploadDropzoneProps } from './upload-dropzone'
import type { UploadDropzoneProps } from '../upload-dropzone'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import UploadDropzone from './upload-dropzone'
import UploadDropzone from '../upload-dropzone'
// Helper to create mock ref objects for testing
const createMockRef = <T,>(value: T | null = null): RefObject<T | null> => ({ current: value })
// Mock react-i18next
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string, options?: Record<string, unknown>) => {
const translations: Record<string, string> = {
'stepOne.uploader.button': 'Drag and drop files, or',
'stepOne.uploader.buttonSingleFile': 'Drag and drop file, or',
'stepOne.uploader.browse': 'Browse',
'stepOne.uploader.tip': 'Supports {{supportTypes}}, Max {{size}}MB each, up to {{batchCount}} files at a time, {{totalCount}} files total',
}
let result = translations[key] || key
if (options && typeof options === 'object') {
Object.entries(options).forEach(([k, v]) => {
result = result.replace(`{{${k}}}`, String(v))
})
}
return result
},
}),
}))
describe('UploadDropzone', () => {
const defaultProps: UploadDropzoneProps = {
dropRef: createMockRef<HTMLDivElement>() as RefObject<HTMLDivElement | null>,
@ -73,17 +52,17 @@ describe('UploadDropzone', () => {
it('should render browse label when extensions are allowed', () => {
render(<UploadDropzone {...defaultProps} />)
expect(screen.getByText('Browse')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepOne.uploader.browse')).toBeInTheDocument()
})
it('should not render browse label when no extensions allowed', () => {
render(<UploadDropzone {...defaultProps} acceptTypes={[]} />)
expect(screen.queryByText('Browse')).not.toBeInTheDocument()
expect(screen.queryByText('datasetCreation.stepOne.uploader.browse')).not.toBeInTheDocument()
})
it('should render file size and count limits', () => {
render(<UploadDropzone {...defaultProps} />)
const tipText = screen.getByText(/Supports.*Max.*15MB/i)
const tipText = screen.getByText(/datasetCreation\.stepOne\.uploader\.tip/)
expect(tipText).toBeInTheDocument()
})
})
@ -111,12 +90,12 @@ describe('UploadDropzone', () => {
describe('text content', () => {
it('should show batch upload text when supportBatchUpload is true', () => {
render(<UploadDropzone {...defaultProps} supportBatchUpload={true} />)
expect(screen.getByText(/Drag and drop files/i)).toBeInTheDocument()
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.button/)).toBeInTheDocument()
})
it('should show single file text when supportBatchUpload is false', () => {
render(<UploadDropzone {...defaultProps} supportBatchUpload={false} />)
expect(screen.getByText(/Drag and drop file/i)).toBeInTheDocument()
expect(screen.getByText(/datasetCreation\.stepOne\.uploader\.buttonSingleFile/)).toBeInTheDocument()
})
})
@ -146,7 +125,7 @@ describe('UploadDropzone', () => {
const onSelectFile = vi.fn()
render(<UploadDropzone {...defaultProps} onSelectFile={onSelectFile} />)
const browseLabel = screen.getByText('Browse')
const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse')
fireEvent.click(browseLabel)
expect(onSelectFile).toHaveBeenCalledTimes(1)
@ -195,7 +174,7 @@ describe('UploadDropzone', () => {
it('should have cursor-pointer on browse label', () => {
render(<UploadDropzone {...defaultProps} />)
const browseLabel = screen.getByText('Browse')
const browseLabel = screen.getByText('datasetCreation.stepOne.uploader.browse')
expect(browseLabel).toHaveClass('cursor-pointer')
})
})

View File

@ -4,15 +4,14 @@ import { act, render, renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ToastContext } from '@/app/components/base/toast'
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../constants'
import { PROGRESS_COMPLETE, PROGRESS_ERROR, PROGRESS_NOT_STARTED } from '../../constants'
// Import after mocks
import { useFileUpload } from './use-file-upload'
import { useFileUpload } from '../use-file-upload'
// Mock notify function
const mockNotify = vi.fn()
const mockClose = vi.fn()
// Mock ToastContext
vi.mock('use-context-selector', async () => {
const actual = await vi.importActual<typeof import('use-context-selector')>('use-context-selector')
return {
@ -44,12 +43,6 @@ vi.mock('@/service/use-common', () => ({
}))
// Mock i18n
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock locale
vi.mock('@/context/i18n', () => ({
useLocale: () => 'en-US',
@ -59,7 +52,6 @@ vi.mock('@/i18n-config/language', () => ({
LanguagesSupported: ['en-US', 'zh-Hans'],
}))
// Mock config
vi.mock('@/config', () => ({
IS_CE_EDITION: false,
}))

View File

@ -2,7 +2,7 @@ import type { MockedFunction } from 'vitest'
import type { NotionPage } from '@/models/common'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fetchNotionPagePreview } from '@/service/datasets'
import NotionPagePreview from './index'
import NotionPagePreview from '../index'
// Mock the fetchNotionPagePreview service
vi.mock('@/service/datasets', () => ({
@ -85,13 +85,10 @@ const findLoadingSpinner = (container: HTMLElement) => {
return container.querySelector('.spin-animation')
}
// ============================================================================
// NotionPagePreview Component Tests
// ============================================================================
// Note: Branch coverage is ~88% because line 29 (`if (!currentPage) return`)
// is defensive code that cannot be reached - getPreviewContent is only called
// from useEffect when currentPage is truthy.
// ============================================================================
describe('NotionPagePreview', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -106,31 +103,23 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// Rendering Tests - Verify component renders properly
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', async () => {
// Arrange & Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
it('should render page preview header', async () => {
// Arrange & Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
it('should render close button with XMarkIcon', async () => {
// Arrange & Act
const { container } = await renderNotionPagePreview()
// Assert
const closeButton = container.querySelector('.cursor-pointer')
expect(closeButton).toBeInTheDocument()
const xMarkIcon = closeButton?.querySelector('svg')
@ -138,30 +127,23 @@ describe('NotionPagePreview', () => {
})
it('should render page name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: 'My Notion Page' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('My Notion Page')).toBeInTheDocument()
})
it('should apply correct CSS classes to container', async () => {
// Arrange & Act
const { container } = await renderNotionPagePreview()
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('h-full')
})
it('should render NotionIcon component', async () => {
// Arrange
const page = createMockNotionPage()
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - NotionIcon should be rendered (either as img or div or svg)
@ -170,15 +152,11 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// NotionIcon Rendering Tests
// --------------------------------------------------------------------------
describe('NotionIcon Rendering', () => {
it('should render default icon when page_icon is null', async () => {
// Arrange
const page = createMockNotionPage({ page_icon: null })
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should render RiFileTextLine icon (svg)
@ -187,33 +165,25 @@ describe('NotionPagePreview', () => {
})
it('should render emoji icon when page_icon has emoji type', async () => {
// Arrange
const page = createMockNotionPageWithEmojiIcon('📝')
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('📝')).toBeInTheDocument()
})
it('should render image icon when page_icon has url type', async () => {
// Arrange
const page = createMockNotionPageWithUrlIcon('https://example.com/icon.png')
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert
const img = container.querySelector('img[alt="page icon"]')
expect(img).toBeInTheDocument()
expect(img).toHaveAttribute('src', 'https://example.com/icon.png')
})
})
// --------------------------------------------------------------------------
// Loading State Tests
// --------------------------------------------------------------------------
describe('Loading State', () => {
it('should show loading indicator initially', async () => {
// Arrange - Delay API response to keep loading state
@ -230,13 +200,10 @@ describe('NotionPagePreview', () => {
})
it('should hide loading indicator after content loads', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Loaded content' })
// Act
const { container } = await renderNotionPagePreview()
// Assert
expect(screen.getByText('Loaded content')).toBeInTheDocument()
// Loading should be gone
const loadingElement = findLoadingSpinner(container)
@ -244,7 +211,6 @@ describe('NotionPagePreview', () => {
})
it('should show loading when currentPage changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1', page_name: 'Page 1' })
const page2 = createMockNotionPage({ page_id: 'page-2', page_name: 'Page 2' })
@ -291,24 +257,19 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// API Call Tests
// --------------------------------------------------------------------------
describe('API Calls', () => {
it('should call fetchNotionPagePreview with correct parameters', async () => {
// Arrange
const page = createMockNotionPage({
page_id: 'test-page-id',
type: 'database',
})
// Act
await renderNotionPagePreview({
currentPage: page,
notionCredentialId: 'test-credential-id',
})
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith({
pageID: 'test-page-id',
pageType: 'database',
@ -317,19 +278,15 @@ describe('NotionPagePreview', () => {
})
it('should not call fetchNotionPagePreview when currentPage is undefined', async () => {
// Arrange & Act
await renderNotionPagePreview({ currentPage: undefined }, false)
// Assert
expect(mockFetchNotionPagePreview).not.toHaveBeenCalled()
})
it('should call fetchNotionPagePreview again when currentPage changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
@ -346,7 +303,6 @@ describe('NotionPagePreview', () => {
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
// Assert
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith({
pageID: 'page-2',
@ -358,21 +314,16 @@ describe('NotionPagePreview', () => {
})
it('should handle API success and display content', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Notion page preview content from API' })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('Notion page preview content from API')).toBeInTheDocument()
})
it('should handle API error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('Network error'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert - Component should not crash
@ -384,10 +335,8 @@ describe('NotionPagePreview', () => {
})
it('should handle empty content response', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: '' })
// Act
const { container } = await renderNotionPagePreview()
// Assert - Should still render without loading
@ -396,42 +345,30 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call hidePreview when close button is clicked', async () => {
// Arrange
const hidePreview = vi.fn()
const { container } = await renderNotionPagePreview({ hidePreview })
// Act
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
// Assert
expect(hidePreview).toHaveBeenCalledTimes(1)
})
it('should handle multiple clicks on close button', async () => {
// Arrange
const hidePreview = vi.fn()
const { container } = await renderNotionPagePreview({ hidePreview })
// Act
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
fireEvent.click(closeButton)
fireEvent.click(closeButton)
// Assert
expect(hidePreview).toHaveBeenCalledTimes(3)
})
})
// --------------------------------------------------------------------------
// State Management Tests
// --------------------------------------------------------------------------
describe('State Management', () => {
it('should initialize with loading state true', async () => {
// Arrange - Keep loading indefinitely (never resolves)
@ -440,24 +377,19 @@ describe('NotionPagePreview', () => {
// Act - Don't wait for content
const { container } = await renderNotionPagePreview({}, false)
// Assert
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).toBeInTheDocument()
})
it('should update previewContent state after successful fetch', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'New preview content' })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('New preview content')).toBeInTheDocument()
})
it('should reset loading to true when currentPage changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
@ -465,7 +397,6 @@ describe('NotionPagePreview', () => {
.mockResolvedValueOnce({ content: 'Content 1' })
.mockImplementationOnce(() => new Promise(() => { /* never resolves */ }))
// Act
const { rerender, container } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
@ -487,7 +418,6 @@ describe('NotionPagePreview', () => {
})
it('should replace old content with new content when page changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
@ -497,7 +427,6 @@ describe('NotionPagePreview', () => {
.mockResolvedValueOnce({ content: 'Content 1' })
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
@ -523,24 +452,17 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// Props Testing
// --------------------------------------------------------------------------
describe('Props', () => {
describe('currentPage prop', () => {
it('should render correctly with currentPage prop', async () => {
// Arrange
const page = createMockNotionPage({ page_name: 'My Test Page' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('My Test Page')).toBeInTheDocument()
})
it('should render correctly without currentPage prop (undefined)', async () => {
// Arrange & Act
await renderNotionPagePreview({ currentPage: undefined }, false)
// Assert - Header should still render
@ -548,10 +470,8 @@ describe('NotionPagePreview', () => {
})
it('should handle page with empty name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: '' })
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should not crash
@ -559,52 +479,40 @@ describe('NotionPagePreview', () => {
})
it('should handle page with very long name', async () => {
// Arrange
const longName = 'a'.repeat(200)
const page = createMockNotionPage({ page_name: longName })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText(longName)).toBeInTheDocument()
})
it('should handle page with special characters in name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: 'Page with <special> & "chars"' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('Page with <special> & "chars"')).toBeInTheDocument()
})
it('should handle page with unicode characters in name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: '中文页面名称 🚀 日本語' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('中文页面名称 🚀 日本語')).toBeInTheDocument()
})
})
describe('notionCredentialId prop', () => {
it('should pass notionCredentialId to API call', async () => {
// Arrange
const page = createMockNotionPage()
// Act
await renderNotionPagePreview({
currentPage: page,
notionCredentialId: 'my-credential-id',
})
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ credentialID: 'my-credential-id' }),
)
@ -613,10 +521,8 @@ describe('NotionPagePreview', () => {
describe('hidePreview prop', () => {
it('should accept hidePreview callback', async () => {
// Arrange
const hidePreview = vi.fn()
// Act
await renderNotionPagePreview({ hidePreview })
// Assert - No errors thrown
@ -625,15 +531,10 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle page with undefined page_id', async () => {
// Arrange
const page = createMockNotionPage({ page_id: undefined as unknown as string })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert - API should still be called (with undefined pageID)
@ -641,36 +542,28 @@ describe('NotionPagePreview', () => {
})
it('should handle page with empty string page_id', async () => {
// Arrange
const page = createMockNotionPage({ page_id: '' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageID: '' }),
)
})
it('should handle very long preview content', async () => {
// Arrange
const longContent = 'x'.repeat(10000)
mockFetchNotionPagePreview.mockResolvedValue({ content: longContent })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText(longContent)).toBeInTheDocument()
})
it('should handle preview content with special characters safely', async () => {
// Arrange
const specialContent = '<script>alert("xss")</script>\n\t& < > "'
mockFetchNotionPagePreview.mockResolvedValue({ content: specialContent })
// Act
const { container } = await renderNotionPagePreview()
// Assert - Should render as text, not execute scripts
@ -680,26 +573,20 @@ describe('NotionPagePreview', () => {
})
it('should handle preview content with unicode', async () => {
// Arrange
const unicodeContent = '中文内容 🚀 émojis & spëcîal çhàrs'
mockFetchNotionPagePreview.mockResolvedValue({ content: unicodeContent })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText(unicodeContent)).toBeInTheDocument()
})
it('should handle preview content with newlines', async () => {
// Arrange
const multilineContent = 'Line 1\nLine 2\nLine 3'
mockFetchNotionPagePreview.mockResolvedValue({ content: multilineContent })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
expect(contentDiv?.textContent).toContain('Line 1')
@ -708,10 +595,8 @@ describe('NotionPagePreview', () => {
})
it('should handle null content from API', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: null as unknown as string })
// Act
const { container } = await renderNotionPagePreview()
// Assert - Should not crash
@ -719,29 +604,22 @@ describe('NotionPagePreview', () => {
})
it('should handle different page types', async () => {
// Arrange
const databasePage = createMockNotionPage({ type: 'database' })
// Act
await renderNotionPagePreview({ currentPage: databasePage })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'database' }),
)
})
})
// --------------------------------------------------------------------------
// Side Effects and Cleanup Tests
// --------------------------------------------------------------------------
describe('Side Effects and Cleanup', () => {
it('should trigger effect when currentPage prop changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
@ -754,19 +632,16 @@ describe('NotionPagePreview', () => {
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
// Assert
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(2)
})
})
it('should not trigger effect when hidePreview changes', async () => {
// Arrange
const page = createMockNotionPage()
const hidePreview1 = vi.fn()
const hidePreview2 = vi.fn()
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page} notionCredentialId="cred-123" hidePreview={hidePreview1} />,
)
@ -785,10 +660,8 @@ describe('NotionPagePreview', () => {
})
it('should not trigger effect when notionCredentialId changes', async () => {
// Arrange
const page = createMockNotionPage()
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page} notionCredentialId="cred-1" hidePreview={vi.fn()} />,
)
@ -806,11 +679,9 @@ describe('NotionPagePreview', () => {
})
it('should handle rapid page changes', async () => {
// Arrange
const pages = Array.from({ length: 5 }, (_, i) =>
createMockNotionPage({ page_id: `page-${i}` }))
// Act
const { rerender } = render(
<NotionPagePreview currentPage={pages[0]} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
@ -829,7 +700,6 @@ describe('NotionPagePreview', () => {
})
it('should handle unmount during loading', async () => {
// Arrange
mockFetchNotionPagePreview.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
)
@ -845,10 +715,8 @@ describe('NotionPagePreview', () => {
})
it('should handle page changing from defined to undefined', async () => {
// Arrange
const page = createMockNotionPage()
// Act
const { rerender, container } = render(
<NotionPagePreview currentPage={page} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
@ -867,38 +735,27 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// Accessibility Tests
// --------------------------------------------------------------------------
describe('Accessibility', () => {
it('should have clickable close button with visual indicator', async () => {
// Arrange & Act
const { container } = await renderNotionPagePreview()
// Assert
const closeButton = container.querySelector('.cursor-pointer')
expect(closeButton).toBeInTheDocument()
expect(closeButton).toHaveClass('cursor-pointer')
})
it('should have proper heading structure', async () => {
// Arrange & Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Error Handling Tests
// --------------------------------------------------------------------------
describe('Error Handling', () => {
it('should not crash on API network error', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('Network Error'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert - Component should still render
@ -908,122 +765,92 @@ describe('NotionPagePreview', () => {
})
it('should not crash on API timeout', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('Timeout'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should not crash on malformed API response', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({} as { content: string })
// Act
const { container } = await renderNotionPagePreview()
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should handle 404 error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('404 Not Found'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should handle 500 error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('500 Internal Server Error'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should handle authorization error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('401 Unauthorized'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
})
// --------------------------------------------------------------------------
// Page Type Variations Tests
// --------------------------------------------------------------------------
describe('Page Type Variations', () => {
it('should handle page type', async () => {
// Arrange
const page = createMockNotionPage({ type: 'page' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'page' }),
)
})
it('should handle database type', async () => {
// Arrange
const page = createMockNotionPage({ type: 'database' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'database' }),
)
})
it('should handle unknown type', async () => {
// Arrange
const page = createMockNotionPage({ type: 'unknown_type' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'unknown_type' }),
)
})
})
// --------------------------------------------------------------------------
// Icon Type Variations Tests
// --------------------------------------------------------------------------
describe('Icon Type Variations', () => {
it('should handle page with null icon', async () => {
// Arrange
const page = createMockNotionPage({ page_icon: null })
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should render default icon
@ -1032,31 +859,24 @@ describe('NotionPagePreview', () => {
})
it('should handle page with emoji icon object', async () => {
// Arrange
const page = createMockNotionPageWithEmojiIcon('📄')
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('📄')).toBeInTheDocument()
})
it('should handle page with url icon object', async () => {
// Arrange
const page = createMockNotionPageWithUrlIcon('https://example.com/custom-icon.png')
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert
const img = container.querySelector('img[alt="page icon"]')
expect(img).toBeInTheDocument()
expect(img).toHaveAttribute('src', 'https://example.com/custom-icon.png')
})
it('should handle page with icon object having null values', async () => {
// Arrange
const page = createMockNotionPage({
page_icon: {
type: null,
@ -1065,7 +885,6 @@ describe('NotionPagePreview', () => {
},
})
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should render, likely with default/fallback
@ -1073,7 +892,6 @@ describe('NotionPagePreview', () => {
})
it('should handle page with icon object having empty url', async () => {
// Arrange
// Suppress console.error for this test as we're intentionally testing empty src edge case
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn())
@ -1085,7 +903,6 @@ describe('NotionPagePreview', () => {
},
})
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Component should not crash, may render img or fallback
@ -1100,32 +917,24 @@ describe('NotionPagePreview', () => {
})
})
// --------------------------------------------------------------------------
// Content Display Tests
// --------------------------------------------------------------------------
describe('Content Display', () => {
it('should display content in fileContent div with correct class', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Test content' })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
expect(contentDiv).toHaveTextContent('Test content')
})
it('should preserve whitespace in content', async () => {
// Arrange
const contentWithWhitespace = ' indented content\n more indent'
mockFetchNotionPagePreview.mockResolvedValue({ content: contentWithWhitespace })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
// The CSS class has white-space: pre-line
@ -1133,13 +942,10 @@ describe('NotionPagePreview', () => {
})
it('should display empty string content without loading', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: '' })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).not.toBeInTheDocument()
const contentDiv = container.querySelector('[class*="fileContent"]')

View File

@ -4,11 +4,7 @@ import type { CrawlOptions, CrawlResultItem, DataSet, FileItem } from '@/models/
import { fireEvent, render, screen } from '@testing-library/react'
import { Plan } from '@/app/components/billing/type'
import { DataSourceType } from '@/models/datasets'
import StepOne from './index'
// ==========================================
// Mock External Dependencies
// ==========================================
import StepOne from '../index'
// Mock config for website crawl features
vi.mock('@/config', () => ({
@ -40,8 +36,7 @@ vi.mock('@/context/provider-context', () => ({
}),
}))
// Mock child components
vi.mock('../file-uploader', () => ({
vi.mock('../../file-uploader', () => ({
default: ({ onPreview, fileList }: { onPreview: (file: File) => void, fileList: FileItem[] }) => (
<div data-testid="file-uploader">
<span data-testid="file-count">{fileList.length}</span>
@ -52,7 +47,7 @@ vi.mock('../file-uploader', () => ({
),
}))
vi.mock('../website', () => ({
vi.mock('../../website', () => ({
default: ({ onPreview }: { onPreview: (item: CrawlResultItem) => void }) => (
<div data-testid="website">
<button
@ -65,7 +60,7 @@ vi.mock('../website', () => ({
),
}))
vi.mock('../empty-dataset-creation-modal', () => ({
vi.mock('../../empty-dataset-creation-modal', () => ({
default: ({ show, onHide }: { show: boolean, onHide: () => void }) => (
show
? (
@ -109,7 +104,7 @@ vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
),
}))
vi.mock('../file-preview', () => ({
vi.mock('../../file-preview', () => ({
default: ({ file, hidePreview }: { file: File, hidePreview: () => void }) => (
<div data-testid="file-preview">
<span>{file.name}</span>
@ -118,7 +113,7 @@ vi.mock('../file-preview', () => ({
),
}))
vi.mock('../notion-page-preview', () => ({
vi.mock('../../notion-page-preview', () => ({
default: ({ currentPage, hidePreview }: { currentPage: NotionPage, hidePreview: () => void }) => (
<div data-testid="notion-page-preview">
<span>{currentPage.page_id}</span>
@ -130,14 +125,10 @@ vi.mock('../notion-page-preview', () => ({
// WebsitePreview is a sibling component without API dependencies - imported directly
// It only depends on i18n which is globally mocked
vi.mock('./upgrade-card', () => ({
vi.mock('../upgrade-card', () => ({
default: () => <div data-testid="upgrade-card">Upgrade Card</div>,
}))
// ==========================================
// Test Data Builders
// ==========================================
const createMockCustomFile = (overrides: { id?: string, name?: string } = {}) => {
const file = new File(['test content'], overrides.name ?? 'test.txt', { type: 'text/plain' })
return Object.assign(file, {
@ -208,7 +199,6 @@ const defaultProps = {
authedDataSourceList: [] as DataSourceAuth[],
}
// ==========================================
// NOTE: Child component unit tests (usePreviewState, DataSourceTypeSelector,
// NextStepButton, PreviewPanel) have been moved to their own dedicated spec files:
// - ./hooks/use-preview-state.spec.ts
@ -216,11 +206,8 @@ const defaultProps = {
// - ./components/next-step-button.spec.tsx
// - ./components/preview-panel.spec.tsx
// This file now focuses exclusively on StepOne parent component tests.
// ==========================================
// ==========================================
// StepOne Component Tests
// ==========================================
describe('StepOne', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -233,36 +220,26 @@ describe('StepOne', () => {
mockEnableBilling = false
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<StepOne {...defaultProps} />)
// Assert
expect(screen.getByText('datasetCreation.steps.one')).toBeInTheDocument()
})
it('should render DataSourceTypeSelector when not editing existing dataset', () => {
// Arrange & Act
render(<StepOne {...defaultProps} />)
// Assert
expect(screen.getByText('datasetCreation.stepOne.dataSourceType.file')).toBeInTheDocument()
})
it('should render FileUploader when dataSourceType is FILE', () => {
// Arrange & Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.FILE} />)
// Assert
expect(screen.getByTestId('file-uploader')).toBeInTheDocument()
})
it('should render NotionConnector when dataSourceType is NOTION and not authenticated', () => {
// Arrange & Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} />)
// Assert - NotionConnector shows sync title and connect button
@ -271,112 +248,81 @@ describe('StepOne', () => {
})
it('should render NotionPageSelector when dataSourceType is NOTION and authenticated', () => {
// Arrange
const authedDataSourceList = [createMockDataSourceAuth()]
// Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
// Assert
expect(screen.getByTestId('notion-page-selector')).toBeInTheDocument()
})
it('should render Website when dataSourceType is WEB', () => {
// Arrange & Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.WEB} />)
// Assert
expect(screen.getByTestId('website')).toBeInTheDocument()
})
it('should render empty dataset creation link when no datasetId', () => {
// Arrange & Act
render(<StepOne {...defaultProps} />)
// Assert
expect(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation')).toBeInTheDocument()
})
it('should not render empty dataset creation link when datasetId exists', () => {
// Arrange & Act
render(<StepOne {...defaultProps} datasetId="dataset-123" />)
// Assert
expect(screen.queryByText('datasetCreation.stepOne.emptyDatasetCreation')).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Props Tests
// --------------------------------------------------------------------------
describe('Props', () => {
it('should pass files to FileUploader', () => {
// Arrange
const files = [createMockFileItem()]
// Act
render(<StepOne {...defaultProps} files={files} />)
// Assert
expect(screen.getByTestId('file-count')).toHaveTextContent('1')
})
it('should call onSetting when NotionConnector connect button is clicked', () => {
// Arrange
const onSetting = vi.fn()
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} onSetting={onSetting} />)
// Act - The NotionConnector's button calls onSetting
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.connect/i }))
// Assert
expect(onSetting).toHaveBeenCalledTimes(1)
})
it('should call changeType when data source type is changed', () => {
// Arrange
const changeType = vi.fn()
render(<StepOne {...defaultProps} changeType={changeType} />)
// Act
fireEvent.click(screen.getByText('datasetCreation.stepOne.dataSourceType.notion'))
// Assert
expect(changeType).toHaveBeenCalledWith(DataSourceType.NOTION)
})
})
// --------------------------------------------------------------------------
// State Management Tests
// --------------------------------------------------------------------------
describe('State Management', () => {
it('should open empty dataset modal when link is clicked', () => {
// Arrange
render(<StepOne {...defaultProps} />)
// Act
fireEvent.click(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation'))
// Assert
expect(screen.getByTestId('empty-dataset-modal')).toBeInTheDocument()
})
it('should close empty dataset modal when close is clicked', () => {
// Arrange
render(<StepOne {...defaultProps} />)
fireEvent.click(screen.getByText('datasetCreation.stepOne.emptyDatasetCreation'))
// Act
fireEvent.click(screen.getByTestId('close-modal'))
// Assert
expect(screen.queryByTestId('empty-dataset-modal')).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should correctly compute isNotionAuthed based on authedDataSourceList', () => {
// Arrange - No auth
@ -388,12 +334,10 @@ describe('StepOne', () => {
const authedDataSourceList = [createMockDataSourceAuth()]
rerender(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
// Assert
expect(screen.getByTestId('notion-page-selector')).toBeInTheDocument()
})
it('should correctly compute fileNextDisabled when files are empty', () => {
// Arrange & Act
render(<StepOne {...defaultProps} files={[]} />)
// Assert - Button should be disabled
@ -401,10 +345,8 @@ describe('StepOne', () => {
})
it('should correctly compute fileNextDisabled when files are loaded', () => {
// Arrange
const files = [createMockFileItem()]
// Act
render(<StepOne {...defaultProps} files={files} />)
// Assert - Button should be enabled
@ -420,7 +362,6 @@ describe('StepOne', () => {
progress: 0,
}
// Act
render(<StepOne {...defaultProps} files={[fileItem]} />)
// Assert - Button should be disabled
@ -428,128 +369,95 @@ describe('StepOne', () => {
})
})
// --------------------------------------------------------------------------
// Callback Tests
// --------------------------------------------------------------------------
describe('Callbacks', () => {
it('should call onStepChange when next button is clicked with valid files', () => {
// Arrange
const onStepChange = vi.fn()
const files = [createMockFileItem()]
render(<StepOne {...defaultProps} files={files} onStepChange={onStepChange} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
// Assert
expect(onStepChange).toHaveBeenCalledTimes(1)
})
it('should show plan upgrade modal when batch upload not supported and multiple files', () => {
// Arrange
mockEnableBilling = true
mockPlan.type = Plan.sandbox
const files = [createMockFileItem(), createMockFileItem()]
render(<StepOne {...defaultProps} files={files} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
// Assert
expect(screen.getByTestId('plan-upgrade-modal')).toBeInTheDocument()
})
it('should show upgrade card when in sandbox plan with files', () => {
// Arrange
mockEnableBilling = true
mockPlan.type = Plan.sandbox
const files = [createMockFileItem()]
// Act
render(<StepOne {...defaultProps} files={files} />)
// Assert
expect(screen.getByTestId('upgrade-card')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Vector Space Full Tests
// --------------------------------------------------------------------------
describe('Vector Space Full', () => {
it('should show VectorSpaceFull when vector space is full and billing is enabled', () => {
// Arrange
mockEnableBilling = true
mockPlan.usage.vectorSpace = 100
mockPlan.total.vectorSpace = 100
const files = [createMockFileItem()]
// Act
render(<StepOne {...defaultProps} files={files} />)
// Assert
expect(screen.getByTestId('vector-space-full')).toBeInTheDocument()
})
it('should disable next button when vector space is full', () => {
// Arrange
mockEnableBilling = true
mockPlan.usage.vectorSpace = 100
mockPlan.total.vectorSpace = 100
const files = [createMockFileItem()]
// Act
render(<StepOne {...defaultProps} files={files} />)
// Assert
expect(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i })).toBeDisabled()
})
})
// --------------------------------------------------------------------------
// Preview Integration Tests
// --------------------------------------------------------------------------
describe('Preview Integration', () => {
it('should show file preview when file preview button is clicked', () => {
// Arrange
render(<StepOne {...defaultProps} />)
// Act
fireEvent.click(screen.getByTestId('preview-file'))
// Assert
expect(screen.getByTestId('file-preview')).toBeInTheDocument()
})
it('should hide file preview when hide button is clicked', () => {
// Arrange
render(<StepOne {...defaultProps} />)
fireEvent.click(screen.getByTestId('preview-file'))
// Act
fireEvent.click(screen.getByTestId('hide-file-preview'))
// Assert
expect(screen.queryByTestId('file-preview')).not.toBeInTheDocument()
})
it('should show notion page preview when preview button is clicked', () => {
// Arrange
const authedDataSourceList = [createMockDataSourceAuth()]
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
// Act
fireEvent.click(screen.getByTestId('preview-notion'))
// Assert
expect(screen.getByTestId('notion-page-preview')).toBeInTheDocument()
})
it('should show website preview when preview button is clicked', () => {
// Arrange
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.WEB} />)
// Act
fireEvent.click(screen.getByTestId('preview-website'))
// Assert - Check for pagePreview title which is shown by WebsitePreview
@ -557,15 +465,10 @@ describe('StepOne', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle empty notionPages array', () => {
// Arrange
const authedDataSourceList = [createMockDataSourceAuth()]
// Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} notionPages={[]} authedDataSourceList={authedDataSourceList} />)
// Assert - Button should be disabled when no pages selected
@ -573,7 +476,6 @@ describe('StepOne', () => {
})
it('should handle empty websitePages array', () => {
// Arrange & Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.WEB} websitePages={[]} />)
// Assert - Button should be disabled when no pages crawled
@ -581,7 +483,6 @@ describe('StepOne', () => {
})
it('should handle empty authedDataSourceList', () => {
// Arrange & Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={[]} />)
// Assert - Should show NotionConnector with connect button
@ -589,10 +490,8 @@ describe('StepOne', () => {
})
it('should handle authedDataSourceList without notion credentials', () => {
// Arrange
const authedDataSourceList = [createMockDataSourceAuth({ credentials_list: [] })]
// Act
render(<StepOne {...defaultProps} dataSourceType={DataSourceType.NOTION} authedDataSourceList={authedDataSourceList} />)
// Assert - Should show NotionConnector with connect button
@ -600,7 +499,6 @@ describe('StepOne', () => {
})
it('should clear previews when switching data source types', () => {
// Arrange
render(<StepOne {...defaultProps} />)
fireEvent.click(screen.getByTestId('preview-file'))
expect(screen.getByTestId('file-preview')).toBeInTheDocument()
@ -613,30 +511,22 @@ describe('StepOne', () => {
})
})
// --------------------------------------------------------------------------
// Integration Tests
// --------------------------------------------------------------------------
describe('Integration', () => {
it('should complete file upload flow', () => {
// Arrange
const onStepChange = vi.fn()
const files = [createMockFileItem()]
// Act
render(<StepOne {...defaultProps} files={files} onStepChange={onStepChange} />)
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
// Assert
expect(onStepChange).toHaveBeenCalled()
})
it('should complete notion page selection flow', () => {
// Arrange
const onStepChange = vi.fn()
const authedDataSourceList = [createMockDataSourceAuth()]
const notionPages = [createMockNotionPage()]
// Act
render(
<StepOne
{...defaultProps}
@ -648,16 +538,13 @@ describe('StepOne', () => {
)
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
// Assert
expect(onStepChange).toHaveBeenCalled()
})
it('should complete website crawl flow', () => {
// Arrange
const onStepChange = vi.fn()
const websitePages = [createMockCrawlResult()]
// Act
render(
<StepOne
{...defaultProps}
@ -668,7 +555,6 @@ describe('StepOne', () => {
)
fireEvent.click(screen.getByRole('button', { name: /datasetCreation.stepOne.button/i }))
// Assert
expect(onStepChange).toHaveBeenCalled()
})
})

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import UpgradeCard from './upgrade-card'
import UpgradeCard from '../upgrade-card'
const mockSetShowPricingModal = vi.fn()
@ -25,7 +25,6 @@ describe('UpgradeCard', () => {
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
render(<UpgradeCard />)
// Assert - title and description i18n keys are rendered
@ -33,73 +32,56 @@ describe('UpgradeCard', () => {
})
it('should render the upgrade title text', () => {
// Arrange & Act
render(<UpgradeCard />)
// Assert
expect(screen.getByText(/uploadMultipleFiles\.title/i)).toBeInTheDocument()
})
it('should render the upgrade description text', () => {
// Arrange & Act
render(<UpgradeCard />)
// Assert
expect(screen.getByText(/uploadMultipleFiles\.description/i)).toBeInTheDocument()
})
it('should render the upgrade button', () => {
// Arrange & Act
render(<UpgradeCard />)
// Assert
expect(screen.getByRole('button')).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call setShowPricingModal when upgrade button is clicked', () => {
// Arrange
render(<UpgradeCard />)
// Act
fireEvent.click(screen.getByRole('button'))
// Assert
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1)
})
it('should not call setShowPricingModal without user interaction', () => {
// Arrange & Act
render(<UpgradeCard />)
// Assert
expect(mockSetShowPricingModal).not.toHaveBeenCalled()
})
it('should call setShowPricingModal on each button click', () => {
// Arrange
render(<UpgradeCard />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
fireEvent.click(button)
// Assert
expect(mockSetShowPricingModal).toHaveBeenCalledTimes(2)
})
})
describe('Memoization', () => {
it('should maintain rendering after rerender with same props', () => {
// Arrange
const { rerender } = render(<UpgradeCard />)
// Act
rerender(<UpgradeCard />)
// Assert
expect(screen.getByText(/uploadMultipleFiles\.title/i)).toBeInTheDocument()
expect(screen.getByRole('button')).toBeInTheDocument()
})

View File

@ -10,7 +10,7 @@ vi.mock('@/config', () => ({
}))
// Mock CSS module
vi.mock('../../index.module.css', () => ({
vi.mock('../../../index.module.css', () => ({
default: {
dataSourceItem: 'ds-item',
active: 'active',
@ -21,7 +21,7 @@ vi.mock('../../index.module.css', () => ({
},
}))
const { default: DataSourceTypeSelector } = await import('./data-source-type-selector')
const { default: DataSourceTypeSelector } = await import('../data-source-type-selector')
describe('DataSourceTypeSelector', () => {
const defaultProps = {

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import NextStepButton from './next-step-button'
import NextStepButton from '../next-step-button'
describe('NextStepButton', () => {
const defaultProps = {

View File

@ -4,7 +4,7 @@ import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Mock child components - paths must match source file's imports (relative to source)
vi.mock('../../file-preview', () => ({
vi.mock('../../../file-preview', () => ({
default: ({ file, hidePreview }: { file: { name: string }, hidePreview: () => void }) => (
<div data-testid="file-preview">
<span>{file.name}</span>
@ -13,7 +13,7 @@ vi.mock('../../file-preview', () => ({
),
}))
vi.mock('../../notion-page-preview', () => ({
vi.mock('../../../notion-page-preview', () => ({
default: ({ currentPage, hidePreview }: { currentPage: { page_name: string }, hidePreview: () => void }) => (
<div data-testid="notion-preview">
<span>{currentPage.page_name}</span>
@ -22,7 +22,7 @@ vi.mock('../../notion-page-preview', () => ({
),
}))
vi.mock('../../website/preview', () => ({
vi.mock('../../../website/preview', () => ({
default: ({ payload, hidePreview }: { payload: { title: string }, hidePreview: () => void }) => (
<div data-testid="website-preview">
<span>{payload.title}</span>
@ -42,7 +42,7 @@ vi.mock('@/app/components/billing/plan-upgrade-modal', () => ({
: null,
}))
const { default: PreviewPanel } = await import('./preview-panel')
const { default: PreviewPanel } = await import('../preview-panel')
describe('PreviewPanel', () => {
const defaultProps = {

View File

@ -2,7 +2,7 @@ import type { NotionPage } from '@/models/common'
import type { CrawlResultItem } from '@/models/datasets'
import { act, renderHook } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import usePreviewState from './use-preview-state'
import usePreviewState from '../use-preview-state'
describe('usePreviewState', () => {
it('should initialize with all previews undefined', () => {

View File

@ -1,10 +1,10 @@
import type { createDocumentResponse, FullDocumentDetail, IconInfo } from '@/models/datasets'
import type { createDocumentResponse, DataSet, FullDocumentDetail, IconInfo } from '@/models/datasets'
import { render, screen } from '@testing-library/react'
import { RETRIEVE_METHOD } from '@/types/app'
import StepThree from './index'
import StepThree from '../index'
// Mock the EmbeddingProcess component since it has complex async logic
vi.mock('../embedding-process', () => ({
vi.mock('../../embedding-process', () => ({
default: vi.fn(({ datasetId, batchId, documents, indexingType, retrievalMethod }) => (
<div data-testid="embedding-process">
<span data-testid="ep-dataset-id">{datasetId}</span>
@ -98,97 +98,74 @@ const renderStepThree = (props: Partial<Parameters<typeof StepThree>[0]> = {}) =
return render(<StepThree {...defaultProps} />)
}
// ============================================================================
// StepThree Component Tests
// ============================================================================
describe('StepThree', () => {
beforeEach(() => {
vi.clearAllMocks()
mockMediaType = 'pc'
})
// --------------------------------------------------------------------------
// Rendering Tests - Verify component renders properly
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
renderStepThree()
// Assert
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
})
it('should render with creation title when datasetId is not provided', () => {
// Arrange & Act
renderStepThree()
// Assert
expect(screen.getByText('datasetCreation.stepThree.creationTitle')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepThree.creationContent')).toBeInTheDocument()
})
it('should render with addition title when datasetId is provided', () => {
// Arrange & Act
renderStepThree({
datasetId: 'existing-dataset-123',
datasetName: 'Existing Dataset',
})
// Assert
expect(screen.getByText('datasetCreation.stepThree.additionTitle')).toBeInTheDocument()
expect(screen.queryByText('datasetCreation.stepThree.creationTitle')).not.toBeInTheDocument()
})
it('should render label text in creation mode', () => {
// Arrange & Act
renderStepThree()
// Assert
expect(screen.getByText('datasetCreation.stepThree.label')).toBeInTheDocument()
})
it('should render side tip panel on desktop', () => {
// Arrange
mockMediaType = 'pc'
// Act
renderStepThree()
// Assert
expect(screen.getByText('datasetCreation.stepThree.sideTipTitle')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepThree.sideTipContent')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')).toBeInTheDocument()
})
it('should not render side tip panel on mobile', () => {
// Arrange
mockMediaType = 'mobile'
// Act
renderStepThree()
// Assert
expect(screen.queryByText('datasetCreation.stepThree.sideTipTitle')).not.toBeInTheDocument()
expect(screen.queryByText('datasetCreation.stepThree.sideTipContent')).not.toBeInTheDocument()
})
it('should render EmbeddingProcess component', () => {
// Arrange & Act
renderStepThree()
// Assert
expect(screen.getByTestId('embedding-process')).toBeInTheDocument()
})
it('should render documentation link with correct href on desktop', () => {
// Arrange
mockMediaType = 'pc'
// Act
renderStepThree()
// Assert
const link = screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')
expect(link).toHaveAttribute('href', 'https://docs.dify.ai/en-US/use-dify/knowledge/integrate-knowledge-within-application')
expect(link).toHaveAttribute('target', '_blank')
@ -196,70 +173,53 @@ describe('StepThree', () => {
})
it('should apply correct container classes', () => {
// Arrange & Act
const { container } = renderStepThree()
// Assert
const outerDiv = container.firstChild as HTMLElement
expect(outerDiv).toHaveClass('flex', 'h-full', 'max-h-full', 'w-full', 'justify-center', 'overflow-y-auto')
})
})
// --------------------------------------------------------------------------
// Props Testing - Test all prop variations
// --------------------------------------------------------------------------
describe('Props', () => {
describe('datasetId prop', () => {
it('should render creation mode when datasetId is undefined', () => {
// Arrange & Act
renderStepThree({ datasetId: undefined })
// Assert
expect(screen.getByText('datasetCreation.stepThree.creationTitle')).toBeInTheDocument()
})
it('should render addition mode when datasetId is provided', () => {
// Arrange & Act
renderStepThree({ datasetId: 'dataset-123' })
// Assert
expect(screen.getByText('datasetCreation.stepThree.additionTitle')).toBeInTheDocument()
})
it('should pass datasetId to EmbeddingProcess', () => {
// Arrange
const datasetId = 'my-dataset-id'
// Act
renderStepThree({ datasetId })
// Assert
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent(datasetId)
})
it('should use creationCache dataset id when datasetId is not provided', () => {
// Arrange
const creationCache = createMockCreationCache()
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('dataset-123')
})
})
describe('datasetName prop', () => {
it('should display datasetName in creation mode', () => {
// Arrange & Act
renderStepThree({ datasetName: 'My Custom Dataset' })
// Assert
expect(screen.getByText('My Custom Dataset')).toBeInTheDocument()
})
it('should display datasetName in addition mode description', () => {
// Arrange & Act
renderStepThree({
datasetId: 'dataset-123',
datasetName: 'Existing Dataset Name',
@ -271,45 +231,35 @@ describe('StepThree', () => {
})
it('should fallback to creationCache dataset name when datasetName is not provided', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.name = 'Cache Dataset Name'
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByText('Cache Dataset Name')).toBeInTheDocument()
})
})
describe('indexingType prop', () => {
it('should pass indexingType to EmbeddingProcess', () => {
// Arrange & Act
renderStepThree({ indexingType: 'high_quality' })
// Assert
expect(screen.getByTestId('ep-indexing-type')).toHaveTextContent('high_quality')
})
it('should use creationCache indexing_technique when indexingType is not provided', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.indexing_technique = 'economy' as any
creationCache.dataset!.indexing_technique = 'economy' as unknown as DataSet['indexing_technique']
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-indexing-type')).toHaveTextContent('economy')
})
it('should prefer creationCache indexing_technique over indexingType prop', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.indexing_technique = 'cache_technique' as any
creationCache.dataset!.indexing_technique = 'cache_technique' as unknown as DataSet['indexing_technique']
// Act
renderStepThree({ creationCache, indexingType: 'prop_technique' })
// Assert - creationCache takes precedence
@ -319,60 +269,47 @@ describe('StepThree', () => {
describe('retrievalMethod prop', () => {
it('should pass retrievalMethod to EmbeddingProcess', () => {
// Arrange & Act
renderStepThree({ retrievalMethod: RETRIEVE_METHOD.semantic })
// Assert
expect(screen.getByTestId('ep-retrieval-method')).toHaveTextContent('semantic_search')
})
it('should use creationCache retrieval method when retrievalMethod is not provided', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.retrieval_model_dict = { search_method: 'full_text_search' } as any
creationCache.dataset!.retrieval_model_dict = { search_method: 'full_text_search' } as unknown as DataSet['retrieval_model_dict']
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-retrieval-method')).toHaveTextContent('full_text_search')
})
})
describe('creationCache prop', () => {
it('should pass batchId from creationCache to EmbeddingProcess', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.batch = 'custom-batch-123'
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('custom-batch-123')
})
it('should pass documents from creationCache to EmbeddingProcess', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.documents = [createMockDocument(), createMockDocument(), createMockDocument()] as any
creationCache.documents = [createMockDocument(), createMockDocument(), createMockDocument()] as unknown as createDocumentResponse['documents']
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('3')
})
it('should use icon_info from creationCache dataset', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.icon_info = createMockIconInfo({
icon: '🚀',
icon_background: '#FF0000',
})
// Act
const { container } = renderStepThree({ creationCache })
// Assert - Check AppIcon component receives correct props
@ -381,7 +318,6 @@ describe('StepThree', () => {
})
it('should handle undefined creationCache', () => {
// Arrange & Act
renderStepThree({ creationCache: undefined })
// Assert - Should not crash, use fallback values
@ -390,14 +326,12 @@ describe('StepThree', () => {
})
it('should handle creationCache with undefined dataset', () => {
// Arrange
const creationCache: createDocumentResponse = {
dataset: undefined,
batch: 'batch-123',
documents: [],
}
// Act
renderStepThree({ creationCache })
// Assert - Should use default icon info
@ -406,12 +340,9 @@ describe('StepThree', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests - Test null, undefined, empty values and boundaries
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle all props being undefined', () => {
// Arrange & Act
renderStepThree({
datasetId: undefined,
datasetName: undefined,
@ -426,7 +357,6 @@ describe('StepThree', () => {
})
it('should handle empty string datasetId', () => {
// Arrange & Act
renderStepThree({ datasetId: '' })
// Assert - Empty string is falsy, should show creation mode
@ -434,7 +364,6 @@ describe('StepThree', () => {
})
it('should handle empty string datasetName', () => {
// Arrange & Act
renderStepThree({ datasetName: '' })
// Assert - Should not crash
@ -442,23 +371,18 @@ describe('StepThree', () => {
})
it('should handle empty documents array in creationCache', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.documents = []
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-documents-count')).toHaveTextContent('0')
})
it('should handle creationCache with missing icon_info', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.icon_info = undefined as any
creationCache.dataset!.icon_info = undefined as unknown as IconInfo
// Act
renderStepThree({ creationCache })
// Assert - Should use default icon info
@ -466,10 +390,8 @@ describe('StepThree', () => {
})
it('should handle very long datasetName', () => {
// Arrange
const longName = 'A'.repeat(500)
// Act
renderStepThree({ datasetName: longName })
// Assert - Should render without crashing
@ -477,10 +399,8 @@ describe('StepThree', () => {
})
it('should handle special characters in datasetName', () => {
// Arrange
const specialName = 'Dataset <script>alert("xss")</script> & "quotes" \'apostrophe\''
// Act
renderStepThree({ datasetName: specialName })
// Assert - Should render safely as text
@ -488,22 +408,17 @@ describe('StepThree', () => {
})
it('should handle unicode characters in datasetName', () => {
// Arrange
const unicodeName = '数据集名称 🚀 émojis & spëcîal çhàrs'
// Act
renderStepThree({ datasetName: unicodeName })
// Assert
expect(screen.getByText(unicodeName)).toBeInTheDocument()
})
it('should handle creationCache with null dataset name', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.name = null as any
creationCache.dataset!.name = null as unknown as string
// Act
const { container } = renderStepThree({ creationCache })
// Assert - Should not crash
@ -511,13 +426,10 @@ describe('StepThree', () => {
})
})
// --------------------------------------------------------------------------
// Conditional Rendering Tests - Test mode switching behavior
// --------------------------------------------------------------------------
describe('Conditional Rendering', () => {
describe('Creation Mode (no datasetId)', () => {
it('should show AppIcon component', () => {
// Arrange & Act
const { container } = renderStepThree()
// Assert - AppIcon should be rendered
@ -526,7 +438,6 @@ describe('StepThree', () => {
})
it('should show Divider component', () => {
// Arrange & Act
const { container } = renderStepThree()
// Assert - Divider should be rendered (it adds hr with specific classes)
@ -535,20 +446,16 @@ describe('StepThree', () => {
})
it('should show dataset name input area', () => {
// Arrange
const datasetName = 'Test Dataset Name'
// Act
renderStepThree({ datasetName })
// Assert
expect(screen.getByText(datasetName)).toBeInTheDocument()
})
})
describe('Addition Mode (with datasetId)', () => {
it('should not show AppIcon component', () => {
// Arrange & Act
renderStepThree({ datasetId: 'dataset-123' })
// Assert - Creation section should not be rendered
@ -556,7 +463,6 @@ describe('StepThree', () => {
})
it('should show addition description with dataset name', () => {
// Arrange & Act
renderStepThree({
datasetId: 'dataset-123',
datasetName: 'My Dataset',
@ -569,10 +475,8 @@ describe('StepThree', () => {
describe('Mobile vs Desktop', () => {
it('should show side panel on tablet', () => {
// Arrange
mockMediaType = 'tablet'
// Act
renderStepThree()
// Assert - Tablet is not mobile, should show side panel
@ -580,21 +484,16 @@ describe('StepThree', () => {
})
it('should not show side panel on mobile', () => {
// Arrange
mockMediaType = 'mobile'
// Act
renderStepThree()
// Assert
expect(screen.queryByText('datasetCreation.stepThree.sideTipTitle')).not.toBeInTheDocument()
})
it('should render EmbeddingProcess on mobile', () => {
// Arrange
mockMediaType = 'mobile'
// Act
renderStepThree()
// Assert - Main content should still be rendered
@ -603,64 +502,48 @@ describe('StepThree', () => {
})
})
// --------------------------------------------------------------------------
// EmbeddingProcess Integration Tests - Verify correct props are passed
// --------------------------------------------------------------------------
describe('EmbeddingProcess Integration', () => {
it('should pass correct datasetId to EmbeddingProcess with datasetId prop', () => {
// Arrange & Act
renderStepThree({ datasetId: 'direct-dataset-id' })
// Assert
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('direct-dataset-id')
})
it('should pass creationCache dataset id when datasetId prop is undefined', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.id = 'cache-dataset-id'
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('cache-dataset-id')
})
it('should pass empty string for datasetId when both sources are undefined', () => {
// Arrange & Act
renderStepThree()
// Assert
expect(screen.getByTestId('ep-dataset-id')).toHaveTextContent('')
})
it('should pass batchId from creationCache', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.batch = 'test-batch-456'
// Act
renderStepThree({ creationCache })
// Assert
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('test-batch-456')
})
it('should pass empty string for batchId when creationCache is undefined', () => {
// Arrange & Act
renderStepThree()
// Assert
expect(screen.getByTestId('ep-batch-id')).toHaveTextContent('')
})
it('should prefer datasetId prop over creationCache dataset id', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.id = 'cache-id'
// Act
renderStepThree({ datasetId: 'prop-id', creationCache })
// Assert - datasetId prop takes precedence
@ -668,12 +551,9 @@ describe('StepThree', () => {
})
})
// --------------------------------------------------------------------------
// Icon Rendering Tests - Verify AppIcon behavior
// --------------------------------------------------------------------------
describe('Icon Rendering', () => {
it('should use default icon info when creationCache is undefined', () => {
// Arrange & Act
const { container } = renderStepThree()
// Assert - Default background color should be applied
@ -683,7 +563,6 @@ describe('StepThree', () => {
})
it('should use icon_info from creationCache when available', () => {
// Arrange
const creationCache = createMockCreationCache()
creationCache.dataset!.icon_info = {
icon: '🎉',
@ -692,7 +571,6 @@ describe('StepThree', () => {
icon_url: '',
}
// Act
const { container } = renderStepThree({ creationCache })
// Assert - Custom background color should be applied
@ -702,11 +580,9 @@ describe('StepThree', () => {
})
it('should use default icon when creationCache dataset icon_info is undefined', () => {
// Arrange
const creationCache = createMockCreationCache()
delete (creationCache.dataset as any).icon_info
delete (creationCache.dataset as Partial<DataSet>).icon_info
// Act
const { container } = renderStepThree({ creationCache })
// Assert - Component should still render with default icon
@ -714,15 +590,11 @@ describe('StepThree', () => {
})
})
// --------------------------------------------------------------------------
// Layout Tests - Verify correct CSS classes and structure
// --------------------------------------------------------------------------
describe('Layout', () => {
it('should have correct outer container classes', () => {
// Arrange & Act
const { container } = renderStepThree()
// Assert
const outerDiv = container.firstChild as HTMLElement
expect(outerDiv).toHaveClass('flex')
expect(outerDiv).toHaveClass('h-full')
@ -730,49 +602,37 @@ describe('StepThree', () => {
})
it('should have correct inner container classes', () => {
// Arrange & Act
const { container } = renderStepThree()
// Assert
const innerDiv = container.querySelector('.max-w-\\[960px\\]')
expect(innerDiv).toBeInTheDocument()
expect(innerDiv).toHaveClass('shrink-0', 'grow')
})
it('should have content wrapper with correct max width', () => {
// Arrange & Act
const { container } = renderStepThree()
// Assert
const contentWrapper = container.querySelector('.max-w-\\[640px\\]')
expect(contentWrapper).toBeInTheDocument()
})
it('should have side tip panel with correct width on desktop', () => {
// Arrange
mockMediaType = 'pc'
// Act
const { container } = renderStepThree()
// Assert
const sidePanel = container.querySelector('.w-\\[328px\\]')
expect(sidePanel).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Accessibility Tests - Verify accessibility features
// --------------------------------------------------------------------------
describe('Accessibility', () => {
it('should have correct link attributes for external documentation link', () => {
// Arrange
mockMediaType = 'pc'
// Act
renderStepThree()
// Assert
const link = screen.getByText('datasetPipeline.addDocuments.stepThree.learnMore')
expect(link.tagName).toBe('A')
expect(link).toHaveAttribute('target', '_blank')
@ -780,35 +640,27 @@ describe('StepThree', () => {
})
it('should have semantic heading structure in creation mode', () => {
// Arrange & Act
renderStepThree()
// Assert
const title = screen.getByText('datasetCreation.stepThree.creationTitle')
expect(title).toBeInTheDocument()
expect(title.className).toContain('title-2xl-semi-bold')
})
it('should have semantic heading structure in addition mode', () => {
// Arrange & Act
renderStepThree({ datasetId: 'dataset-123' })
// Assert
const title = screen.getByText('datasetCreation.stepThree.additionTitle')
expect(title).toBeInTheDocument()
expect(title.className).toContain('title-2xl-semi-bold')
})
})
// --------------------------------------------------------------------------
// Side Panel Tests - Verify side panel behavior
// --------------------------------------------------------------------------
describe('Side Panel', () => {
it('should render RiBookOpenLine icon in side panel', () => {
// Arrange
mockMediaType = 'pc'
// Act
const { container } = renderStepThree()
// Assert - Icon should be present in side panel
@ -817,25 +669,19 @@ describe('StepThree', () => {
})
it('should have correct side panel section background', () => {
// Arrange
mockMediaType = 'pc'
// Act
const { container } = renderStepThree()
// Assert
const sidePanel = container.querySelector('.bg-background-section')
expect(sidePanel).toBeInTheDocument()
})
it('should have correct padding for side panel', () => {
// Arrange
mockMediaType = 'pc'
// Act
const { container } = renderStepThree()
// Assert
const sidePanelWrapper = container.querySelector('.pr-8')
expect(sidePanelWrapper).toBeInTheDocument()
})

View File

@ -14,8 +14,8 @@ import { act, cleanup, fireEvent, render, renderHook, screen } from '@testing-li
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { ChunkingMode, DataSourceType, ProcessMode } from '@/models/datasets'
import { RETRIEVE_METHOD } from '@/types/app'
import { PreviewPanel } from './components/preview-panel'
import { StepTwoFooter } from './components/step-two-footer'
import { PreviewPanel } from '../components/preview-panel'
import { StepTwoFooter } from '../components/step-two-footer'
import {
DEFAULT_MAXIMUM_CHUNK_LENGTH,
DEFAULT_OVERLAP,
@ -27,10 +27,10 @@ import {
useIndexingEstimate,
usePreviewState,
useSegmentationState,
} from './hooks'
import escape from './hooks/escape'
import unescape from './hooks/unescape'
import StepTwo from './index'
} from '../hooks'
import escape from '../hooks/escape'
import unescape from '../hooks/unescape'
import StepTwo from '../index'
const mockDataset = {
id: 'test-dataset-id',
@ -91,7 +91,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
useDefaultModel: () => ({ data: mockDefaultEmbeddingModel }),
}))
// Mock service hooks
const mockFetchDefaultProcessRuleMutate = vi.fn()
vi.mock('@/service/knowledge/use-create-dataset', () => ({
useFetchDefaultProcessRule: ({ onSuccess }: { onSuccess: (data: { rules: Rules, limits: { indexing_max_segmentation_tokens_length: number } }) => void }) => ({
@ -277,9 +276,7 @@ const createMockEstimate = (overrides?: Partial<FileIndexingEstimateResponse>):
...overrides,
})
// ============================================
// Utility Functions Tests (escape/unescape)
// ============================================
describe('escape utility', () => {
beforeEach(() => {
@ -738,9 +735,7 @@ describe('useSegmentationState', () => {
})
})
// ============================================
// useIndexingConfig Hook Tests
// ============================================
describe('useIndexingConfig', () => {
beforeEach(() => {
@ -912,9 +907,7 @@ describe('useIndexingConfig', () => {
})
})
// ============================================
// usePreviewState Hook Tests
// ============================================
describe('usePreviewState', () => {
beforeEach(() => {
@ -1141,9 +1134,7 @@ describe('usePreviewState', () => {
})
})
// ============================================
// useDocumentCreation Hook Tests
// ============================================
describe('useDocumentCreation', () => {
beforeEach(() => {
@ -1565,9 +1556,7 @@ describe('useDocumentCreation', () => {
})
})
// ============================================
// useIndexingEstimate Hook Tests
// ============================================
describe('useIndexingEstimate', () => {
beforeEach(() => {
@ -1707,9 +1696,7 @@ describe('useIndexingEstimate', () => {
})
})
// ============================================
// StepTwoFooter Component Tests
// ============================================
describe('StepTwoFooter', () => {
beforeEach(() => {
@ -1799,9 +1786,7 @@ describe('StepTwoFooter', () => {
})
})
// ============================================
// PreviewPanel Component Tests
// ============================================
describe('PreviewPanel', () => {
beforeEach(() => {
@ -1980,10 +1965,6 @@ describe('PreviewPanel', () => {
})
})
// ============================================
// Edge Cases Tests
// ============================================
describe('Edge Cases', () => {
beforeEach(() => {
vi.clearAllMocks()
@ -2097,9 +2078,7 @@ describe('Edge Cases', () => {
})
})
// ============================================
// Integration Scenarios
// ============================================
describe('Integration Scenarios', () => {
beforeEach(() => {
@ -2221,9 +2200,7 @@ describe('Integration Scenarios', () => {
})
})
// ============================================
// StepTwo Component Tests
// ============================================
describe('StepTwo Component', () => {
beforeEach(() => {
@ -2525,7 +2502,6 @@ describe('StepTwo Component', () => {
await vi.waitFor(() => {
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
})
// Click parentChild option to trigger handleDocFormChange(ChunkingMode.parentChild) with ECONOMICAL
const parentChildTitles = screen.getAllByText(/stepTwo\.parentChild/i)
fireEvent.click(parentChildTitles[0])
})
@ -2535,7 +2511,6 @@ describe('StepTwo Component', () => {
await vi.waitFor(() => {
expect(screen.getByText(/stepTwo\.segmentation/i)).toBeInTheDocument()
})
// Click QA checkbox (visible because IS_CE_EDITION is mocked as true)
const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i)
fireEvent.click(qaCheckbox)
// Dialog should open → click Switch to confirm (triggers handleQAConfirm)
@ -2552,7 +2527,6 @@ describe('StepTwo Component', () => {
// Open QA confirm dialog
const qaCheckbox = screen.getByText(/stepTwo\.useQALanguage/i)
fireEvent.click(qaCheckbox)
// Click the dialog cancel button (onQAConfirmDialogClose)
const dialogCancelButtons = await screen.findAllByText(/stepTwo\.cancel/i)
fireEvent.click(dialogCancelButtons[0])
})
@ -2563,7 +2537,6 @@ describe('StepTwo Component', () => {
createMockFile({ id: 'file-2', name: 'second.pdf', extension: 'pdf' }),
]
render(<StepTwo {...defaultStepTwoProps} files={files} />)
// Click on the second file in the mocked picker (triggers handlePickerChange)
const pickerButton = screen.getByTestId('picker-file-2')
fireEvent.click(pickerButton)
})
@ -2573,7 +2546,6 @@ describe('StepTwo Component', () => {
document.body.setAttribute('data-public-indexing-max-segmentation-tokens-length', '100')
render(<StepTwo {...defaultStepTwoProps} />)
// The default maxChunkLength (1024) now exceeds the limit (100)
// Click preview button to trigger updatePreview error path
const previewButtons = screen.getAllByText(/stepTwo\.previewChunk/i)
fireEvent.click(previewButtons[0])
// Restore

View File

@ -2,13 +2,7 @@ import type { PreProcessingRule } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import { GeneralChunkingOptions } from './general-chunking-options'
vi.mock('next/image', () => ({
default: ({ alt, ...props }: { alt?: string, src?: string, width?: number, height?: number }) => (
<img alt={alt} {...props} />
),
}))
import { GeneralChunkingOptions } from '../general-chunking-options'
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
default: ({ onSummaryIndexSettingChange }: { onSummaryIndexSettingChange?: (val: Record<string, unknown>) => void }) => (

View File

@ -3,14 +3,8 @@ import type { RetrievalConfig } from '@/types/app'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import { IndexingType } from '../hooks'
import { IndexingModeSection } from './indexing-mode-section'
vi.mock('next/image', () => ({
default: ({ alt, ...props }: { alt?: string, src?: string, width?: number, height?: number }) => (
<img alt={alt} {...props} />
),
}))
import { IndexingType } from '../../hooks'
import { IndexingModeSection } from '../indexing-mode-section'
vi.mock('next/link', () => ({
default: ({ children, href, ...props }: { children?: React.ReactNode, href?: string, className?: string }) => <a href={href} {...props}>{children}</a>,

View File

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DelimiterInput, MaxLengthInput, OverlapInput } from './inputs'
import { DelimiterInput, MaxLengthInput, OverlapInput } from '../inputs'
// i18n mock returns namespaced keys like "datasetCreation.stepTwo.separator"
const ns = 'datasetCreation'

View File

@ -1,7 +1,8 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { OptionCard, OptionCardHeader } from './option-card'
import { OptionCard, OptionCardHeader } from '../option-card'
// Override global next/image auto-mock: tests assert on rendered <img> elements
vi.mock('next/image', () => ({
default: ({ src, alt, ...props }: { src?: string, alt?: string, width?: number, height?: number }) => (
<img src={src} alt={alt} {...props} />

View File

@ -1,15 +1,9 @@
import type { ParentChildConfig } from '../hooks'
import type { ParentChildConfig } from '../../hooks'
import type { PreProcessingRule } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ChunkingMode } from '@/models/datasets'
import { ParentChildOptions } from './parent-child-options'
vi.mock('next/image', () => ({
default: ({ alt, ...props }: { alt?: string, src?: string, width?: number, height?: number }) => (
<img alt={alt} {...props} />
),
}))
import { ParentChildOptions } from '../parent-child-options'
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
default: ({ onSummaryIndexSettingChange }: { onSummaryIndexSettingChange?: (val: Record<string, unknown>) => void }) => (

View File

@ -1,19 +1,9 @@
import type { ParentChildConfig } from '../hooks'
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" />,
}))
import { PreviewPanel } from '../preview-panel'
vi.mock('@/app/components/base/float-right-container', () => ({
default: ({ children }: { children: React.ReactNode }) => <div data-testid="float-container">{children}</div>,
@ -30,7 +20,7 @@ vi.mock('@/app/components/base/skeleton', () => ({
SkeletonRow: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
}))
vi.mock('../../../chunk', () => ({
vi.mock('../../../../chunk', () => ({
ChunkContainer: ({ children, label }: { children: React.ReactNode, label: string }) => (
<div data-testid="chunk-container">
{label}
@ -42,15 +32,15 @@ vi.mock('../../../chunk', () => ({
QAPreview: ({ qa }: { qa: { question: string } }) => <div data-testid="qa-preview">{qa.question}</div>,
}))
vi.mock('../../../common/document-picker/preview-document-picker', () => ({
vi.mock('../../../../common/document-picker/preview-document-picker', () => ({
default: () => <div data-testid="doc-picker" />,
}))
vi.mock('../../../documents/detail/completed/common/summary-label', () => ({
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', () => ({
vi.mock('../../../../formatted-text/flavours/preview-slice', () => ({
PreviewSlice: ({ label, text }: { label: string, text: string }) => (
<span data-testid="preview-slice">
{label}
@ -61,11 +51,11 @@ vi.mock('../../../formatted-text/flavours/preview-slice', () => ({
),
}))
vi.mock('../../../formatted-text/formatted', () => ({
vi.mock('../../../../formatted-text/formatted', () => ({
FormattedText: ({ children }: { children: React.ReactNode }) => <p data-testid="formatted-text">{children}</p>,
}))
vi.mock('../../../preview/container', () => ({
vi.mock('../../../../preview/container', () => ({
default: ({ children, header }: { children: React.ReactNode, header: React.ReactNode }) => (
<div data-testid="preview-container">
{header}
@ -74,7 +64,7 @@ vi.mock('../../../preview/container', () => ({
),
}))
vi.mock('../../../preview/header', () => ({
vi.mock('../../../../preview/header', () => ({
PreviewHeader: ({ children, title }: { children: React.ReactNode, title: string }) => (
<div data-testid="preview-header">
{title}
@ -106,7 +96,7 @@ describe('PreviewPanel', () => {
it('should render preview header with title', () => {
render(<PreviewPanel {...defaultProps} />)
expect(screen.getByTestId('preview-header')).toHaveTextContent('stepTwo.preview')
expect(screen.getByTestId('preview-header')).toHaveTextContent('datasetCreation.stepTwo.preview')
})
it('should render document picker', () => {
@ -116,7 +106,7 @@ describe('PreviewPanel', () => {
it('should show idle state when isIdle is true', () => {
render(<PreviewPanel {...defaultProps} isIdle={true} />)
expect(screen.getByText('stepTwo.previewChunkTip')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepTwo.previewChunkTip')).toBeInTheDocument()
})
it('should show loading skeletons when isPending', () => {

View File

@ -1,13 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { StepTwoFooter } from './step-two-footer'
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: (key: string) => key }),
}))
vi.mock('@remixicon/react', () => ({
RiArrowLeftLine: (props: React.SVGProps<SVGSVGElement>) => <svg data-testid="arrow-left" {...props} />,
}))
import { StepTwoFooter } from '../step-two-footer'
describe('StepTwoFooter', () => {
const defaultProps = {
@ -23,31 +16,31 @@ describe('StepTwoFooter', () => {
it('should render previous and next buttons when not isSetting', () => {
render(<StepTwoFooter {...defaultProps} />)
expect(screen.getByText('stepTwo.previousStep')).toBeInTheDocument()
expect(screen.getByText('stepTwo.nextStep')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepTwo.previousStep')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepTwo.nextStep')).toBeInTheDocument()
})
it('should render save and cancel buttons when isSetting', () => {
render(<StepTwoFooter {...defaultProps} isSetting />)
expect(screen.getByText('stepTwo.save')).toBeInTheDocument()
expect(screen.getByText('stepTwo.cancel')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepTwo.save')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepTwo.cancel')).toBeInTheDocument()
})
it('should call onPrevious on previous button click', () => {
render(<StepTwoFooter {...defaultProps} />)
fireEvent.click(screen.getByText('stepTwo.previousStep'))
fireEvent.click(screen.getByText('datasetCreation.stepTwo.previousStep'))
expect(defaultProps.onPrevious).toHaveBeenCalledOnce()
})
it('should call onCreate on next button click', () => {
render(<StepTwoFooter {...defaultProps} />)
fireEvent.click(screen.getByText('stepTwo.nextStep'))
fireEvent.click(screen.getByText('datasetCreation.stepTwo.nextStep'))
expect(defaultProps.onCreate).toHaveBeenCalledOnce()
})
it('should call onCancel on cancel button click in settings mode', () => {
render(<StepTwoFooter {...defaultProps} isSetting />)
fireEvent.click(screen.getByText('stepTwo.cancel'))
fireEvent.click(screen.getByText('datasetCreation.stepTwo.cancel'))
expect(defaultProps.onCancel).toHaveBeenCalledOnce()
})
})

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import escape from './escape'
import escape from '../escape'
describe('escape', () => {
// Basic special character escaping
@ -44,7 +44,6 @@ describe('escape', () => {
expect(escape('\n\r\t')).toBe('\\n\\r\\t')
})
// Edge cases
it('should return empty string for null input', () => {
expect(escape(null as unknown as string)).toBe('')
})

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import unescape from './unescape'
import unescape from '../unescape'
describe('unescape', () => {
// Basic escape sequences
@ -86,7 +86,6 @@ describe('unescape', () => {
expect(unescape('before\\nafter')).toBe('before\nafter')
})
// Edge cases
it('should handle empty string', () => {
expect(unescape('')).toBe('')
})

View File

@ -37,8 +37,8 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
useInvalidDatasetList: () => mocks.invalidDatasetList,
}))
const { useDocumentCreation } = await import('./use-document-creation')
const { IndexingType } = await import('./use-indexing-config')
const { useDocumentCreation } = await import('../use-document-creation')
const { IndexingType } = await import('../use-indexing-config')
describe('useDocumentCreation', () => {
const defaultOptions = {

View File

@ -25,7 +25,7 @@ vi.mock('@/app/components/datasets/settings/utils', () => ({
checkShowMultiModalTip: vi.fn(() => false),
}))
const { IndexingType, useIndexingConfig } = await import('./use-indexing-config')
const { IndexingType, useIndexingConfig } = await import('../use-indexing-config')
describe('useIndexingConfig', () => {
const defaultOptions = {

View File

@ -1,4 +1,4 @@
import type { IndexingType } from './use-indexing-config'
import type { IndexingType } from '../use-indexing-config'
import type { NotionPage } from '@/models/common'
import type { ChunkingMode, CrawlResultItem, CustomFile, ProcessRule } from '@/models/datasets'
import { renderHook } from '@testing-library/react'
@ -39,7 +39,7 @@ vi.mock('@/service/knowledge/use-create-dataset', () => ({
}),
}))
const { useIndexingEstimate } = await import('./use-indexing-estimate')
const { useIndexingEstimate } = await import('../use-indexing-estimate')
describe('useIndexingEstimate', () => {
const defaultOptions = {

View File

@ -3,7 +3,7 @@ import type { CrawlResultItem, CustomFile } from '@/models/datasets'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DataSourceType } from '@/models/datasets'
import { usePreviewState } from './use-preview-state'
import { usePreviewState } from '../use-preview-state'
// Factory functions
const createFile = (id: string, name: string): CustomFile => ({

View File

@ -8,7 +8,7 @@ import {
DEFAULT_SEGMENT_IDENTIFIER,
defaultParentChildConfig,
useSegmentationState,
} from './use-segmentation-state'
} from '../use-segmentation-state'
describe('useSegmentationState', () => {
beforeEach(() => {
@ -239,7 +239,6 @@ describe('useSegmentationState', () => {
result.current.setMaxChunkLength(2048)
result.current.setOverlap(200)
})
// Reset
act(() => {
result.current.resetToDefaults()
})

View File

@ -1,8 +1,8 @@
import type { ILanguageSelectProps } from './index'
import type { ILanguageSelectProps } from '../index'
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { languages } from '@/i18n-config/language'
import LanguageSelect from './index'
import LanguageSelect from '../index'
// Get supported languages for test assertions
const supportedLanguages = languages.filter(lang => lang.supported)
@ -20,37 +20,27 @@ describe('LanguageSelect', () => {
vi.clearAllMocks()
})
// ==========================================
// Rendering Tests - Verify component renders correctly
// ==========================================
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<LanguageSelect {...props} />)
// Assert
expect(screen.getByText('English')).toBeInTheDocument()
})
it('should render current language text', () => {
// Arrange
const props = createDefaultProps({ currentLanguage: 'Chinese Simplified' })
// Act
render(<LanguageSelect {...props} />)
// Assert
expect(screen.getByText('Chinese Simplified')).toBeInTheDocument()
})
it('should render dropdown arrow icon', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<LanguageSelect {...props} />)
// Assert - RiArrowDownSLine renders as SVG
@ -59,7 +49,6 @@ describe('LanguageSelect', () => {
})
it('should render all supported languages in dropdown when opened', () => {
// Arrange
const props = createDefaultProps()
render(<LanguageSelect {...props} />)
@ -75,12 +64,10 @@ describe('LanguageSelect', () => {
})
it('should render check icon for selected language', () => {
// Arrange
const selectedLanguage = 'Japanese'
const props = createDefaultProps({ currentLanguage: selectedLanguage })
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -91,9 +78,7 @@ describe('LanguageSelect', () => {
})
})
// ==========================================
// Props Testing - Verify all prop variations work correctly
// ==========================================
describe('Props', () => {
describe('currentLanguage prop', () => {
it('should display English when currentLanguage is English', () => {
@ -126,47 +111,36 @@ describe('LanguageSelect', () => {
describe('disabled prop', () => {
it('should have disabled button when disabled is true', () => {
// Arrange
const props = createDefaultProps({ disabled: true })
// Act
render(<LanguageSelect {...props} />)
// Assert
const button = screen.getByRole('button')
expect(button).toBeDisabled()
})
it('should have enabled button when disabled is false', () => {
// Arrange
const props = createDefaultProps({ disabled: false })
// Act
render(<LanguageSelect {...props} />)
// Assert
const button = screen.getByRole('button')
expect(button).not.toBeDisabled()
})
it('should have enabled button when disabled is undefined', () => {
// Arrange
const props = createDefaultProps()
delete (props as Partial<ILanguageSelectProps>).disabled
// Act
render(<LanguageSelect {...props} />)
// Assert
const button = screen.getByRole('button')
expect(button).not.toBeDisabled()
})
it('should apply disabled styling when disabled is true', () => {
// Arrange
const props = createDefaultProps({ disabled: true })
// Act
const { container } = render(<LanguageSelect {...props} />)
// Assert - Check for disabled class on text elements
@ -175,13 +149,10 @@ describe('LanguageSelect', () => {
})
it('should apply cursor-not-allowed styling when disabled', () => {
// Arrange
const props = createDefaultProps({ disabled: true })
// Act
const { container } = render(<LanguageSelect {...props} />)
// Assert
const elementWithCursor = container.querySelector('.cursor-not-allowed')
expect(elementWithCursor).toBeInTheDocument()
})
@ -205,16 +176,12 @@ describe('LanguageSelect', () => {
})
})
// ==========================================
// User Interactions - Test event handlers
// ==========================================
describe('User Interactions', () => {
it('should open dropdown when button is clicked', () => {
// Arrange
const props = createDefaultProps()
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -223,24 +190,20 @@ describe('LanguageSelect', () => {
})
it('should call onSelect when a language option is clicked', () => {
// Arrange
const mockOnSelect = vi.fn()
const props = createDefaultProps({ onSelect: mockOnSelect })
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
const frenchOption = screen.getByText('French')
fireEvent.click(frenchOption)
// Assert
expect(mockOnSelect).toHaveBeenCalledTimes(1)
expect(mockOnSelect).toHaveBeenCalledWith('French')
})
it('should call onSelect with correct language when selecting different languages', () => {
// Arrange
const mockOnSelect = vi.fn()
const props = createDefaultProps({ onSelect: mockOnSelect })
render(<LanguageSelect {...props} />)
@ -259,11 +222,9 @@ describe('LanguageSelect', () => {
})
it('should not open dropdown when disabled', () => {
// Arrange
const props = createDefaultProps({ disabled: true })
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -273,21 +234,17 @@ describe('LanguageSelect', () => {
})
it('should not call onSelect when component is disabled', () => {
// Arrange
const mockOnSelect = vi.fn()
const props = createDefaultProps({ onSelect: mockOnSelect, disabled: true })
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
// Assert
expect(mockOnSelect).not.toHaveBeenCalled()
})
it('should handle rapid consecutive clicks', () => {
// Arrange
const mockOnSelect = vi.fn()
const props = createDefaultProps({ onSelect: mockOnSelect })
render(<LanguageSelect {...props} />)
@ -303,9 +260,7 @@ describe('LanguageSelect', () => {
})
})
// ==========================================
// Component Memoization - Test React.memo behavior
// ==========================================
describe('Memoization', () => {
it('should be wrapped with React.memo', () => {
// Assert - Check component has memo wrapper
@ -313,7 +268,6 @@ describe('LanguageSelect', () => {
})
it('should not re-render when props remain the same', () => {
// Arrange
const mockOnSelect = vi.fn()
const props = createDefaultProps({ onSelect: mockOnSelect })
const renderSpy = vi.fn()
@ -325,7 +279,6 @@ describe('LanguageSelect', () => {
}
const MemoizedTracked = React.memo(TrackedLanguageSelect)
// Act
const { rerender } = render(<MemoizedTracked {...props} />)
rerender(<MemoizedTracked {...props} />)
@ -334,43 +287,33 @@ describe('LanguageSelect', () => {
})
it('should re-render when currentLanguage changes', () => {
// Arrange
const props = createDefaultProps({ currentLanguage: 'English' })
// Act
const { rerender } = render(<LanguageSelect {...props} />)
expect(screen.getByText('English')).toBeInTheDocument()
rerender(<LanguageSelect {...props} currentLanguage="French" />)
// Assert
expect(screen.getByText('French')).toBeInTheDocument()
})
it('should re-render when disabled changes', () => {
// Arrange
const props = createDefaultProps({ disabled: false })
// Act
const { rerender } = render(<LanguageSelect {...props} />)
expect(screen.getByRole('button')).not.toBeDisabled()
rerender(<LanguageSelect {...props} disabled={true} />)
// Assert
expect(screen.getByRole('button')).toBeDisabled()
})
})
// ==========================================
// Edge Cases - Test boundary conditions and error handling
// ==========================================
describe('Edge Cases', () => {
it('should handle empty string as currentLanguage', () => {
// Arrange
const props = createDefaultProps({ currentLanguage: '' })
// Act
render(<LanguageSelect {...props} />)
// Assert - Component should still render
@ -379,10 +322,8 @@ describe('LanguageSelect', () => {
})
it('should handle non-existent language as currentLanguage', () => {
// Arrange
const props = createDefaultProps({ currentLanguage: 'NonExistentLanguage' })
// Act
render(<LanguageSelect {...props} />)
// Assert - Should display the value even if not in list
@ -393,19 +334,15 @@ describe('LanguageSelect', () => {
// Arrange - Turkish has special character in prompt_name
const props = createDefaultProps({ currentLanguage: 'Türkçe' })
// Act
render(<LanguageSelect {...props} />)
// Assert
expect(screen.getByText('Türkçe')).toBeInTheDocument()
})
it('should handle very long language names', () => {
// Arrange
const longLanguageName = 'A'.repeat(100)
const props = createDefaultProps({ currentLanguage: longLanguageName })
// Act
render(<LanguageSelect {...props} />)
// Assert - Should not crash and should display the text
@ -413,11 +350,9 @@ describe('LanguageSelect', () => {
})
it('should render correct number of language options', () => {
// Arrange
const props = createDefaultProps()
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -431,11 +366,9 @@ describe('LanguageSelect', () => {
})
it('should only show supported languages in dropdown', () => {
// Arrange
const props = createDefaultProps()
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -452,7 +385,6 @@ describe('LanguageSelect', () => {
// Arrange - This tests TypeScript boundary, but runtime should not crash
const props = createDefaultProps()
// Act
render(<LanguageSelect {...props} />)
const button = screen.getByRole('button')
fireEvent.click(button)
@ -463,11 +395,9 @@ describe('LanguageSelect', () => {
})
it('should maintain selection state visually with check icon', () => {
// Arrange
const props = createDefaultProps({ currentLanguage: 'Russian' })
const { container } = render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -478,28 +408,21 @@ describe('LanguageSelect', () => {
})
})
// ==========================================
// Accessibility - Basic accessibility checks
// ==========================================
describe('Accessibility', () => {
it('should have accessible button element', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<LanguageSelect {...props} />)
// Assert
const button = screen.getByRole('button')
expect(button).toBeInTheDocument()
})
it('should have clickable language options', () => {
// Arrange
const props = createDefaultProps()
render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -509,16 +432,12 @@ describe('LanguageSelect', () => {
})
})
// ==========================================
// Integration with Popover - Test Popover behavior
// ==========================================
describe('Popover Integration', () => {
it('should use manualClose prop on Popover', () => {
// Arrange
const mockOnSelect = vi.fn()
const props = createDefaultProps({ onSelect: mockOnSelect })
// Act
render(<LanguageSelect {...props} />)
const button = screen.getByRole('button')
fireEvent.click(button)
@ -528,11 +447,9 @@ describe('LanguageSelect', () => {
})
it('should have correct popup z-index class', () => {
// Arrange
const props = createDefaultProps()
const { container } = render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -542,12 +459,9 @@ describe('LanguageSelect', () => {
})
})
// ==========================================
// Styling Tests - Verify correct CSS classes applied
// ==========================================
describe('Styling', () => {
it('should apply tertiary button styling', () => {
// Arrange
const props = createDefaultProps()
const { container } = render(<LanguageSelect {...props} />)
@ -556,11 +470,9 @@ describe('LanguageSelect', () => {
})
it('should apply hover styling class to options', () => {
// Arrange
const props = createDefaultProps()
const { container } = render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -570,11 +482,9 @@ describe('LanguageSelect', () => {
})
it('should apply correct text styling to language options', () => {
// Arrange
const props = createDefaultProps()
const { container } = render(<LanguageSelect {...props} />)
// Act
const button = screen.getByRole('button')
fireEvent.click(button)
@ -584,7 +494,6 @@ describe('LanguageSelect', () => {
})
it('should apply disabled styling to icon when disabled', () => {
// Arrange
const props = createDefaultProps({ disabled: true })
const { container } = render(<LanguageSelect {...props} />)

View File

@ -1,7 +1,7 @@
import type { IPreviewItemProps } from './index'
import type { IPreviewItemProps } from '../index'
import { render, screen } from '@testing-library/react'
import * as React from 'react'
import PreviewItem, { PreviewType } from './index'
import PreviewItem, { PreviewType } from '../index'
// Test data builder for props
const createDefaultProps = (overrides?: Partial<IPreviewItemProps>): IPreviewItemProps => ({
@ -26,40 +26,29 @@ describe('PreviewItem', () => {
vi.clearAllMocks()
})
// ==========================================
// Rendering Tests - Verify component renders correctly
// ==========================================
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Test content')).toBeInTheDocument()
})
it('should render with TEXT type', () => {
// Arrange
const props = createDefaultProps({ content: 'Sample text content' })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Sample text content')).toBeInTheDocument()
})
it('should render with QA type', () => {
// Arrange
const props = createQAProps()
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
expect(screen.getByText('Test question')).toBeInTheDocument()
@ -67,10 +56,8 @@ describe('PreviewItem', () => {
})
it('should render sharp icon (#) with formatted index', () => {
// Arrange
const props = createDefaultProps({ index: 5 })
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert - Index should be padded to 3 digits
@ -81,11 +68,9 @@ describe('PreviewItem', () => {
})
it('should render character count for TEXT type', () => {
// Arrange
const content = 'Hello World' // 11 characters
const props = createDefaultProps({ content })
// Act
render(<PreviewItem {...props} />)
// Assert - Shows character count with translation key
@ -94,7 +79,6 @@ describe('PreviewItem', () => {
})
it('should render character count for QA type', () => {
// Arrange
const props = createQAProps({
qa: {
question: 'Hello', // 5 characters
@ -102,7 +86,6 @@ describe('PreviewItem', () => {
},
})
// Act
render(<PreviewItem {...props} />)
// Assert - Shows combined character count
@ -110,10 +93,8 @@ describe('PreviewItem', () => {
})
it('should render text icon SVG', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert - Should have SVG icons
@ -122,35 +103,27 @@ describe('PreviewItem', () => {
})
})
// ==========================================
// Props Testing - Verify all prop variations work correctly
// ==========================================
describe('Props', () => {
describe('type prop', () => {
it('should render TEXT content when type is TEXT', () => {
// Arrange
const props = createDefaultProps({ type: PreviewType.TEXT, content: 'Text mode content' })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Text mode content')).toBeInTheDocument()
expect(screen.queryByText('Q')).not.toBeInTheDocument()
expect(screen.queryByText('A')).not.toBeInTheDocument()
})
it('should render QA content when type is QA', () => {
// Arrange
const props = createQAProps({
type: PreviewType.QA,
qa: { question: 'My question', answer: 'My answer' },
})
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
expect(screen.getByText('My question')).toBeInTheDocument()
@ -158,24 +131,18 @@ describe('PreviewItem', () => {
})
it('should use TEXT as default type when type is "text"', () => {
// Arrange
const props = createDefaultProps({ type: 'text' as PreviewType, content: 'Default type content' })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Default type content')).toBeInTheDocument()
})
it('should use QA type when type is "QA"', () => {
// Arrange
const props = createQAProps({ type: 'QA' as PreviewType })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
})
@ -191,57 +158,43 @@ describe('PreviewItem', () => {
[999, '999'],
[1000, '1000'],
])('should format index %i as %s', (index, expected) => {
// Arrange
const props = createDefaultProps({ index })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText(expected)).toBeInTheDocument()
})
it('should handle index 0', () => {
// Arrange
const props = createDefaultProps({ index: 0 })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('000')).toBeInTheDocument()
})
it('should handle large index numbers', () => {
// Arrange
const props = createDefaultProps({ index: 12345 })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('12345')).toBeInTheDocument()
})
})
describe('content prop', () => {
it('should render content when provided', () => {
// Arrange
const props = createDefaultProps({ content: 'Custom content here' })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Custom content here')).toBeInTheDocument()
})
it('should handle multiline content', () => {
// Arrange
const multilineContent = 'Line 1\nLine 2\nLine 3'
const props = createDefaultProps({ content: multilineContent })
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert - Check content is rendered (multiline text is in pre-line div)
@ -252,10 +205,8 @@ describe('PreviewItem', () => {
})
it('should preserve whitespace with pre-line style', () => {
// Arrange
const props = createDefaultProps({ content: 'Text with spaces' })
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert - Check for whiteSpace: pre-line style
@ -266,7 +217,6 @@ describe('PreviewItem', () => {
describe('qa prop', () => {
it('should render question and answer when qa is provided', () => {
// Arrange
const props = createQAProps({
qa: {
question: 'What is testing?',
@ -274,28 +224,22 @@ describe('PreviewItem', () => {
},
})
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('What is testing?')).toBeInTheDocument()
expect(screen.getByText('Testing is verification.')).toBeInTheDocument()
})
it('should render Q and A labels', () => {
// Arrange
const props = createQAProps()
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
})
it('should handle multiline question', () => {
// Arrange
const props = createQAProps({
qa: {
question: 'Question line 1\nQuestion line 2',
@ -303,7 +247,6 @@ describe('PreviewItem', () => {
},
})
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert - Check content is in pre-line div
@ -314,7 +257,6 @@ describe('PreviewItem', () => {
})
it('should handle multiline answer', () => {
// Arrange
const props = createQAProps({
qa: {
question: 'Question',
@ -322,7 +264,6 @@ describe('PreviewItem', () => {
},
})
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert - Check content is in pre-line div
@ -334,9 +275,7 @@ describe('PreviewItem', () => {
})
})
// ==========================================
// Component Memoization - Test React.memo behavior
// ==========================================
describe('Memoization', () => {
it('should be wrapped with React.memo', () => {
// Assert - Check component has memo wrapper
@ -344,7 +283,6 @@ describe('PreviewItem', () => {
})
it('should not re-render when props remain the same', () => {
// Arrange
const props = createDefaultProps()
const renderSpy = vi.fn()
@ -355,7 +293,6 @@ describe('PreviewItem', () => {
}
const MemoizedTracked = React.memo(TrackedPreviewItem)
// Act
const { rerender } = render(<MemoizedTracked {...props} />)
rerender(<MemoizedTracked {...props} />)
@ -364,77 +301,61 @@ describe('PreviewItem', () => {
})
it('should re-render when content changes', () => {
// Arrange
const props = createDefaultProps({ content: 'Initial content' })
// Act
const { rerender } = render(<PreviewItem {...props} />)
expect(screen.getByText('Initial content')).toBeInTheDocument()
rerender(<PreviewItem {...props} content="Updated content" />)
// Assert
expect(screen.getByText('Updated content')).toBeInTheDocument()
})
it('should re-render when index changes', () => {
// Arrange
const props = createDefaultProps({ index: 1 })
// Act
const { rerender } = render(<PreviewItem {...props} />)
expect(screen.getByText('001')).toBeInTheDocument()
rerender(<PreviewItem {...props} index={99} />)
// Assert
expect(screen.getByText('099')).toBeInTheDocument()
})
it('should re-render when type changes', () => {
// Arrange
const props = createDefaultProps({ type: PreviewType.TEXT, content: 'Text content' })
// Act
const { rerender } = render(<PreviewItem {...props} />)
expect(screen.getByText('Text content')).toBeInTheDocument()
expect(screen.queryByText('Q')).not.toBeInTheDocument()
rerender(<PreviewItem type={PreviewType.QA} index={1} qa={{ question: 'Q1', answer: 'A1' }} />)
// Assert
expect(screen.getByText('Q')).toBeInTheDocument()
expect(screen.getByText('A')).toBeInTheDocument()
})
it('should re-render when qa prop changes', () => {
// Arrange
const props = createQAProps({
qa: { question: 'Original question', answer: 'Original answer' },
})
// Act
const { rerender } = render(<PreviewItem {...props} />)
expect(screen.getByText('Original question')).toBeInTheDocument()
rerender(<PreviewItem {...props} qa={{ question: 'New question', answer: 'New answer' }} />)
// Assert
expect(screen.getByText('New question')).toBeInTheDocument()
expect(screen.getByText('New answer')).toBeInTheDocument()
})
})
// ==========================================
// Edge Cases - Test boundary conditions and error handling
// ==========================================
describe('Edge Cases', () => {
describe('Empty/Undefined values', () => {
it('should handle undefined content gracefully', () => {
// Arrange
const props = createDefaultProps({ content: undefined })
// Act
render(<PreviewItem {...props} />)
// Assert - Should show 0 characters (use more specific text match)
@ -442,10 +363,8 @@ describe('PreviewItem', () => {
})
it('should handle empty string content', () => {
// Arrange
const props = createDefaultProps({ content: '' })
// Act
render(<PreviewItem {...props} />)
// Assert - Should show 0 characters (use more specific text match)
@ -453,14 +372,12 @@ describe('PreviewItem', () => {
})
it('should handle undefined qa gracefully', () => {
// Arrange
const props: IPreviewItemProps = {
type: PreviewType.QA,
index: 1,
qa: undefined,
}
// Act
render(<PreviewItem {...props} />)
// Assert - Should render Q and A labels but with empty content
@ -471,7 +388,6 @@ describe('PreviewItem', () => {
})
it('should handle undefined question in qa', () => {
// Arrange
const props: IPreviewItemProps = {
type: PreviewType.QA,
index: 1,
@ -481,15 +397,12 @@ describe('PreviewItem', () => {
},
}
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Only answer')).toBeInTheDocument()
})
it('should handle undefined answer in qa', () => {
// Arrange
const props: IPreviewItemProps = {
type: PreviewType.QA,
index: 1,
@ -499,20 +412,16 @@ describe('PreviewItem', () => {
},
}
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Only question')).toBeInTheDocument()
})
it('should handle empty question and answer strings', () => {
// Arrange
const props = createQAProps({
qa: { question: '', answer: '' },
})
// Act
render(<PreviewItem {...props} />)
// Assert - Should show 0 characters (use more specific text match)
@ -527,10 +436,8 @@ describe('PreviewItem', () => {
// Arrange - 'Test' has 4 characters
const props = createDefaultProps({ content: 'Test' })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText(/4/)).toBeInTheDocument()
})
@ -540,10 +447,8 @@ describe('PreviewItem', () => {
qa: { question: 'ABC', answer: 'DEFGH' },
})
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText(/8/)).toBeInTheDocument()
})
@ -551,10 +456,8 @@ describe('PreviewItem', () => {
// Arrange - Content with special characters
const props = createDefaultProps({ content: '你好世界' }) // 4 Chinese characters
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText(/4/)).toBeInTheDocument()
})
@ -562,10 +465,8 @@ describe('PreviewItem', () => {
// Arrange - 'a\nb' has 3 characters
const props = createDefaultProps({ content: 'a\nb' })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText(/3/)).toBeInTheDocument()
})
@ -573,21 +474,17 @@ describe('PreviewItem', () => {
// Arrange - 'a b' has 3 characters
const props = createDefaultProps({ content: 'a b' })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText(/3/)).toBeInTheDocument()
})
})
describe('Boundary conditions', () => {
it('should handle very long content', () => {
// Arrange
const longContent = 'A'.repeat(10000)
const props = createDefaultProps({ content: longContent })
// Act
render(<PreviewItem {...props} />)
// Assert - Should show correct character count
@ -595,21 +492,16 @@ describe('PreviewItem', () => {
})
it('should handle very long index', () => {
// Arrange
const props = createDefaultProps({ index: 999999999 })
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('999999999')).toBeInTheDocument()
})
it('should handle negative index', () => {
// Arrange
const props = createDefaultProps({ index: -1 })
// Act
render(<PreviewItem {...props} />)
// Assert - padStart pads from the start, so -1 becomes 0-1
@ -617,21 +509,16 @@ describe('PreviewItem', () => {
})
it('should handle content with only whitespace', () => {
// Arrange
const props = createDefaultProps({ content: ' ' }) // 3 spaces
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText(/3/)).toBeInTheDocument()
})
it('should handle content with HTML-like characters', () => {
// Arrange
const props = createDefaultProps({ content: '<div>Test</div>' })
// Act
render(<PreviewItem {...props} />)
// Assert - Should render as text, not HTML
@ -642,7 +529,6 @@ describe('PreviewItem', () => {
// Arrange - Emojis can have complex character lengths
const props = createDefaultProps({ content: '😀👍' })
// Act
render(<PreviewItem {...props} />)
// Assert - Emoji length depends on JS string length
@ -660,17 +546,14 @@ describe('PreviewItem', () => {
qa: { question: 'Should not show', answer: 'Also should not show' },
}
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.getByText('Text content')).toBeInTheDocument()
expect(screen.queryByText('Should not show')).not.toBeInTheDocument()
expect(screen.queryByText('Also should not show')).not.toBeInTheDocument()
})
it('should use content length for TEXT type even when qa is provided', () => {
// Arrange
const props: IPreviewItemProps = {
type: PreviewType.TEXT,
index: 1,
@ -678,7 +561,6 @@ describe('PreviewItem', () => {
qa: { question: 'Question', answer: 'Answer' }, // Would be 14 characters if used
}
// Act
render(<PreviewItem {...props} />)
// Assert - Should show 2, not 14
@ -686,7 +568,6 @@ describe('PreviewItem', () => {
})
it('should ignore content prop when type is QA', () => {
// Arrange
const props: IPreviewItemProps = {
type: PreviewType.QA,
index: 1,
@ -694,10 +575,8 @@ describe('PreviewItem', () => {
qa: { question: 'Q text', answer: 'A text' },
}
// Act
render(<PreviewItem {...props} />)
// Assert
expect(screen.queryByText('Should not display')).not.toBeInTheDocument()
expect(screen.getByText('Q text')).toBeInTheDocument()
expect(screen.getByText('A text')).toBeInTheDocument()
@ -705,9 +584,7 @@ describe('PreviewItem', () => {
})
})
// ==========================================
// PreviewType Enum - Test exported enum values
// ==========================================
describe('PreviewType Enum', () => {
it('should have TEXT value as "text"', () => {
expect(PreviewType.TEXT).toBe('text')
@ -718,27 +595,20 @@ describe('PreviewItem', () => {
})
})
// ==========================================
// Styling Tests - Verify correct CSS classes applied
// ==========================================
describe('Styling', () => {
it('should have rounded container with gray background', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert
const rootDiv = container.firstChild as HTMLElement
expect(rootDiv).toHaveClass('rounded-xl', 'bg-gray-50', 'p-4')
})
it('should have proper header styling', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert - Check header div styling
@ -747,53 +617,40 @@ describe('PreviewItem', () => {
})
it('should have index badge styling', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert
const indexBadge = container.querySelector('.border.border-gray-200')
expect(indexBadge).toBeInTheDocument()
expect(indexBadge).toHaveClass('rounded-md', 'italic', 'font-medium')
})
it('should have content area with line-clamp', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert
const contentArea = container.querySelector('.line-clamp-6')
expect(contentArea).toBeInTheDocument()
expect(contentArea).toHaveClass('max-h-[120px]', 'overflow-hidden')
})
it('should have Q/A labels with gray color', () => {
// Arrange
const props = createQAProps()
// Act
const { container } = render(<PreviewItem {...props} />)
// Assert
const labels = container.querySelectorAll('.text-gray-400')
expect(labels.length).toBeGreaterThanOrEqual(2) // Q and A labels
})
})
// ==========================================
// i18n Translation - Test translation integration
// ==========================================
describe('i18n Translation', () => {
it('should use translation key for characters label', () => {
// Arrange
const props = createDefaultProps({ content: 'Test' })
// Act
render(<PreviewItem {...props} />)
// Assert - The mock returns the key as-is

View File

@ -1,8 +1,8 @@
import type { StepperProps } from './index'
import type { Step, StepperStepProps } from './step'
import type { StepperProps } from '../index'
import type { Step, StepperStepProps } from '../step'
import { render, screen } from '@testing-library/react'
import { Stepper } from './index'
import { StepperStep } from './step'
import { Stepper } from '../index'
import { StepperStep } from '../step'
// Test data factory for creating steps
const createStep = (overrides: Partial<Step> = {}): Step => ({
@ -34,44 +34,33 @@ const renderStepperStep = (props: Partial<StepperStepProps> = {}) => {
return render(<StepperStep {...defaultProps} />)
}
// ============================================================================
// Stepper Component Tests
// ============================================================================
describe('Stepper', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests - Verify component renders properly with various inputs
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
renderStepper()
// Assert
expect(screen.getByText('Step 1')).toBeInTheDocument()
})
it('should render all step names', () => {
// Arrange
const steps = createSteps(3, 'Custom Step')
// Act
renderStepper({ steps })
// Assert
expect(screen.getByText('Custom Step 1')).toBeInTheDocument()
expect(screen.getByText('Custom Step 2')).toBeInTheDocument()
expect(screen.getByText('Custom Step 3')).toBeInTheDocument()
})
it('should render dividers between steps', () => {
// Arrange
const steps = createSteps(3)
// Act
const { container } = renderStepper({ steps })
// Assert - Should have 2 dividers for 3 steps
@ -80,10 +69,8 @@ describe('Stepper', () => {
})
it('should not render divider after last step', () => {
// Arrange
const steps = createSteps(2)
// Act
const { container } = renderStepper({ steps })
// Assert - Should have 1 divider for 2 steps
@ -92,28 +79,21 @@ describe('Stepper', () => {
})
it('should render with flex container layout', () => {
// Arrange & Act
const { container } = renderStepper()
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex', 'items-center', 'gap-3')
})
})
// --------------------------------------------------------------------------
// Props Testing - Test all prop variations and combinations
// --------------------------------------------------------------------------
describe('Props', () => {
describe('steps prop', () => {
it('should render correct number of steps', () => {
// Arrange
const steps = createSteps(5)
// Act
renderStepper({ steps })
// Assert
expect(screen.getByText('Step 1')).toBeInTheDocument()
expect(screen.getByText('Step 2')).toBeInTheDocument()
expect(screen.getByText('Step 3')).toBeInTheDocument()
@ -122,13 +102,10 @@ describe('Stepper', () => {
})
it('should handle single step correctly', () => {
// Arrange
const steps = [createStep({ name: 'Only Step' })]
// Act
const { container } = renderStepper({ steps, activeIndex: 0 })
// Assert
expect(screen.getByText('Only Step')).toBeInTheDocument()
// No dividers for single step
const dividers = container.querySelectorAll('.bg-divider-deep')
@ -136,29 +113,23 @@ describe('Stepper', () => {
})
it('should handle steps with long names', () => {
// Arrange
const longName = 'This is a very long step name that might overflow'
const steps = [createStep({ name: longName })]
// Act
renderStepper({ steps, activeIndex: 0 })
// Assert
expect(screen.getByText(longName)).toBeInTheDocument()
})
it('should handle steps with special characters', () => {
// Arrange
const steps = [
createStep({ name: 'Step & Configuration' }),
createStep({ name: 'Step <Preview>' }),
createStep({ name: 'Step "Complete"' }),
]
// Act
renderStepper({ steps, activeIndex: 0 })
// Assert
expect(screen.getByText('Step & Configuration')).toBeInTheDocument()
expect(screen.getByText('Step <Preview>')).toBeInTheDocument()
expect(screen.getByText('Step "Complete"')).toBeInTheDocument()
@ -167,7 +138,6 @@ describe('Stepper', () => {
describe('activeIndex prop', () => {
it('should highlight first step when activeIndex is 0', () => {
// Arrange & Act
renderStepper({ activeIndex: 0 })
// Assert - First step should show "STEP 1" label
@ -175,7 +145,6 @@ describe('Stepper', () => {
})
it('should highlight second step when activeIndex is 1', () => {
// Arrange & Act
renderStepper({ activeIndex: 1 })
// Assert - Second step should show "STEP 2" label
@ -183,10 +152,8 @@ describe('Stepper', () => {
})
it('should highlight last step when activeIndex equals steps length - 1', () => {
// Arrange
const steps = createSteps(3)
// Act
renderStepper({ steps, activeIndex: 2 })
// Assert - Third step should show "STEP 3" label
@ -194,10 +161,8 @@ describe('Stepper', () => {
})
it('should show completed steps with number only (no STEP prefix)', () => {
// Arrange
const steps = createSteps(3)
// Act
renderStepper({ steps, activeIndex: 2 })
// Assert - Completed steps show just the number
@ -207,10 +172,8 @@ describe('Stepper', () => {
})
it('should show disabled steps with number only (no STEP prefix)', () => {
// Arrange
const steps = createSteps(3)
// Act
renderStepper({ steps, activeIndex: 0 })
// Assert - Disabled steps show just the number
@ -221,12 +184,9 @@ describe('Stepper', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases - Test boundary conditions and unexpected inputs
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle empty steps array', () => {
// Arrange & Act
const { container } = renderStepper({ steps: [] })
// Assert - Container should render but be empty
@ -235,7 +195,6 @@ describe('Stepper', () => {
})
it('should handle activeIndex greater than steps length', () => {
// Arrange
const steps = createSteps(2)
// Act - activeIndex 5 is beyond array bounds
@ -247,7 +206,6 @@ describe('Stepper', () => {
})
it('should handle negative activeIndex', () => {
// Arrange
const steps = createSteps(2)
// Act - negative activeIndex
@ -259,13 +217,10 @@ describe('Stepper', () => {
})
it('should handle large number of steps', () => {
// Arrange
const steps = createSteps(10)
// Act
const { container } = renderStepper({ steps, activeIndex: 5 })
// Assert
expect(screen.getByText('STEP 6')).toBeInTheDocument()
// Should have 9 dividers for 10 steps
const dividers = container.querySelectorAll('.bg-divider-deep')
@ -273,10 +228,8 @@ describe('Stepper', () => {
})
it('should handle steps with empty name', () => {
// Arrange
const steps = [createStep({ name: '' })]
// Act
const { container } = renderStepper({ steps, activeIndex: 0 })
// Assert - Should still render the step structure
@ -285,18 +238,13 @@ describe('Stepper', () => {
})
})
// --------------------------------------------------------------------------
// Integration - Test step state combinations
// --------------------------------------------------------------------------
describe('Step States', () => {
it('should render mixed states: completed, active, disabled', () => {
// Arrange
const steps = createSteps(5)
// Act
renderStepper({ steps, activeIndex: 2 })
// Assert
// Steps 1-2 are completed (show number only)
expect(screen.getByText('1')).toBeInTheDocument()
expect(screen.getByText('2')).toBeInTheDocument()
@ -308,7 +256,6 @@ describe('Stepper', () => {
})
it('should transition through all states correctly', () => {
// Arrange
const steps = createSteps(3)
// Act & Assert - Step 1 active
@ -329,80 +276,59 @@ describe('Stepper', () => {
})
})
// ============================================================================
// StepperStep Component Tests
// ============================================================================
describe('StepperStep', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
renderStepperStep()
// Assert
expect(screen.getByText('Test Step')).toBeInTheDocument()
})
it('should render step name', () => {
// Arrange & Act
renderStepperStep({ name: 'Configure Dataset' })
// Assert
expect(screen.getByText('Configure Dataset')).toBeInTheDocument()
})
it('should render with flex container layout', () => {
// Arrange & Act
const { container } = renderStepperStep()
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex', 'items-center', 'gap-2')
})
})
// --------------------------------------------------------------------------
// Active State Tests
// --------------------------------------------------------------------------
describe('Active State', () => {
it('should show STEP prefix when active', () => {
// Arrange & Act
renderStepperStep({ index: 0, activeIndex: 0 })
// Assert
expect(screen.getByText('STEP 1')).toBeInTheDocument()
})
it('should apply active styles to label container', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 0, activeIndex: 0 })
// Assert
const labelContainer = container.querySelector('.bg-state-accent-solid')
expect(labelContainer).toBeInTheDocument()
expect(labelContainer).toHaveClass('px-2')
})
it('should apply active text color to label', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 0, activeIndex: 0 })
// Assert
const label = container.querySelector('.text-text-primary-on-surface')
expect(label).toBeInTheDocument()
})
it('should apply accent text color to name when active', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 0, activeIndex: 0 })
// Assert
const nameElement = container.querySelector('.text-text-accent')
expect(nameElement).toBeInTheDocument()
expect(nameElement).toHaveClass('system-xs-semibold-uppercase')
@ -421,105 +347,79 @@ describe('StepperStep', () => {
})
})
// --------------------------------------------------------------------------
// Completed State Tests (index < activeIndex)
// --------------------------------------------------------------------------
describe('Completed State', () => {
it('should show number only when completed (not active)', () => {
// Arrange & Act
renderStepperStep({ index: 0, activeIndex: 1 })
// Assert
expect(screen.getByText('1')).toBeInTheDocument()
expect(screen.queryByText('STEP 1')).not.toBeInTheDocument()
})
it('should apply completed styles to label container', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 0, activeIndex: 1 })
// Assert
const labelContainer = container.querySelector('.border-text-quaternary')
expect(labelContainer).toBeInTheDocument()
expect(labelContainer).toHaveClass('w-5')
})
it('should apply tertiary text color to label when completed', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 0, activeIndex: 1 })
// Assert
const label = container.querySelector('.text-text-tertiary')
expect(label).toBeInTheDocument()
})
it('should apply tertiary text color to name when completed', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 0, activeIndex: 2 })
// Assert
const nameElements = container.querySelectorAll('.text-text-tertiary')
expect(nameElements.length).toBeGreaterThan(0)
})
})
// --------------------------------------------------------------------------
// Disabled State Tests (index > activeIndex)
// --------------------------------------------------------------------------
describe('Disabled State', () => {
it('should show number only when disabled', () => {
// Arrange & Act
renderStepperStep({ index: 2, activeIndex: 0 })
// Assert
expect(screen.getByText('3')).toBeInTheDocument()
expect(screen.queryByText('STEP 3')).not.toBeInTheDocument()
})
it('should apply disabled styles to label container', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 2, activeIndex: 0 })
// Assert
const labelContainer = container.querySelector('.border-divider-deep')
expect(labelContainer).toBeInTheDocument()
expect(labelContainer).toHaveClass('w-5')
})
it('should apply quaternary text color to label when disabled', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 2, activeIndex: 0 })
// Assert
const label = container.querySelector('.text-text-quaternary')
expect(label).toBeInTheDocument()
})
it('should apply quaternary text color to name when disabled', () => {
// Arrange & Act
const { container } = renderStepperStep({ index: 2, activeIndex: 0 })
// Assert
const nameElements = container.querySelectorAll('.text-text-quaternary')
expect(nameElements.length).toBeGreaterThan(0)
})
})
// --------------------------------------------------------------------------
// Props Testing
// --------------------------------------------------------------------------
describe('Props', () => {
describe('name prop', () => {
it('should render provided name', () => {
// Arrange & Act
renderStepperStep({ name: 'Custom Name' })
// Assert
expect(screen.getByText('Custom Name')).toBeInTheDocument()
})
it('should handle empty name', () => {
// Arrange & Act
const { container } = renderStepperStep({ name: '' })
// Assert - Label should still render
@ -528,36 +428,28 @@ describe('StepperStep', () => {
})
it('should handle name with whitespace', () => {
// Arrange & Act
renderStepperStep({ name: ' Padded Name ' })
// Assert
expect(screen.getByText('Padded Name')).toBeInTheDocument()
})
})
describe('index prop', () => {
it('should display correct 1-based number for index 0', () => {
// Arrange & Act
renderStepperStep({ index: 0, activeIndex: 0 })
// Assert
expect(screen.getByText('STEP 1')).toBeInTheDocument()
})
it('should display correct 1-based number for index 9', () => {
// Arrange & Act
renderStepperStep({ index: 9, activeIndex: 9 })
// Assert
expect(screen.getByText('STEP 10')).toBeInTheDocument()
})
it('should handle large index values', () => {
// Arrange & Act
renderStepperStep({ index: 99, activeIndex: 99 })
// Assert
expect(screen.getByText('STEP 100')).toBeInTheDocument()
})
})
@ -581,20 +473,14 @@ describe('StepperStep', () => {
})
})
// --------------------------------------------------------------------------
// Edge Cases
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle zero index correctly', () => {
// Arrange & Act
renderStepperStep({ index: 0, activeIndex: 0 })
// Assert
expect(screen.getByText('STEP 1')).toBeInTheDocument()
})
it('should handle negative activeIndex', () => {
// Arrange & Act
renderStepperStep({ index: 0, activeIndex: -1 })
// Assert - Step should be disabled (index > activeIndex)
@ -602,7 +488,6 @@ describe('StepperStep', () => {
})
it('should handle equal boundary (index equals activeIndex)', () => {
// Arrange & Act
renderStepperStep({ index: 5, activeIndex: 5 })
// Assert - Should be active
@ -610,7 +495,6 @@ describe('StepperStep', () => {
})
it('should handle name with HTML-like content safely', () => {
// Arrange & Act
renderStepperStep({ name: '<script>alert("xss")</script>' })
// Assert - Should render as text, not execute
@ -618,73 +502,57 @@ describe('StepperStep', () => {
})
it('should handle name with unicode characters', () => {
// Arrange & Act
renderStepperStep({ name: 'Step 数据 🚀' })
// Assert
expect(screen.getByText('Step 数据 🚀')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Style Classes Verification
// --------------------------------------------------------------------------
describe('Style Classes', () => {
it('should apply correct typography classes to label', () => {
// Arrange & Act
const { container } = renderStepperStep()
// Assert
const label = container.querySelector('.system-2xs-semibold-uppercase')
expect(label).toBeInTheDocument()
})
it('should apply correct typography classes to name', () => {
// Arrange & Act
const { container } = renderStepperStep()
// Assert
const name = container.querySelector('.system-xs-medium-uppercase')
expect(name).toBeInTheDocument()
})
it('should have rounded pill shape for label container', () => {
// Arrange & Act
const { container } = renderStepperStep()
// Assert
const labelContainer = container.querySelector('.rounded-3xl')
expect(labelContainer).toBeInTheDocument()
})
it('should apply h-5 height to label container', () => {
// Arrange & Act
const { container } = renderStepperStep()
// Assert
const labelContainer = container.querySelector('.h-5')
expect(labelContainer).toBeInTheDocument()
})
})
})
// ============================================================================
// Integration Tests - Stepper and StepperStep working together
// ============================================================================
describe('Stepper Integration', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should pass correct props to each StepperStep', () => {
// Arrange
const steps = [
createStep({ name: 'First' }),
createStep({ name: 'Second' }),
createStep({ name: 'Third' }),
]
// Act
renderStepper({ steps, activeIndex: 1 })
// Assert - Each step receives correct index and displays correctly
@ -697,10 +565,8 @@ describe('Stepper Integration', () => {
})
it('should maintain correct visual hierarchy across steps', () => {
// Arrange
const steps = createSteps(4)
// Act
const { container } = renderStepper({ steps, activeIndex: 2 })
// Assert - Check visual hierarchy
@ -718,10 +584,8 @@ describe('Stepper Integration', () => {
})
it('should render correctly with dynamic step updates', () => {
// Arrange
const initialSteps = createSteps(2)
// Act
const { rerender } = render(<Stepper steps={initialSteps} activeIndex={0} />)
expect(screen.getByText('Step 1')).toBeInTheDocument()
expect(screen.getByText('Step 2')).toBeInTheDocument()
@ -730,7 +594,6 @@ describe('Stepper Integration', () => {
const updatedSteps = createSteps(4)
rerender(<Stepper steps={updatedSteps} activeIndex={2} />)
// Assert
expect(screen.getByText('STEP 3')).toBeInTheDocument()
expect(screen.getByText('Step 4')).toBeInTheDocument()
})

View File

@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { StepperStep } from './step'
import { StepperStep } from '../step'
describe('StepperStep', () => {
it('should render step name', () => {

View File

@ -1,6 +1,6 @@
import type { MockInstance } from 'vitest'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import StopEmbeddingModal from './index'
import StopEmbeddingModal from '../index'
// Helper type for component props
type StopEmbeddingModalProps = {
@ -23,9 +23,7 @@ const renderStopEmbeddingModal = (props: Partial<StopEmbeddingModalProps> = {})
}
}
// ============================================================================
// StopEmbeddingModal Component Tests
// ============================================================================
describe('StopEmbeddingModal', () => {
// Suppress Headless UI warnings in tests
// These warnings are from the library's internal behavior, not our code
@ -37,69 +35,54 @@ describe('StopEmbeddingModal', () => {
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn())
})
beforeEach(() => {
vi.clearAllMocks()
})
afterAll(() => {
consoleWarnSpy.mockRestore()
consoleErrorSpy.mockRestore()
})
beforeEach(() => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests - Verify component renders properly
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing when show is true', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
})
it('should render modal title', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
})
it('should render modal content', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeInTheDocument()
})
it('should render confirm button with correct text', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelButtonConfirm')).toBeInTheDocument()
})
it('should render cancel button with correct text', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelButtonCancel')).toBeInTheDocument()
})
it('should not render modal content when show is false', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: false })
// Assert
expect(screen.queryByText('datasetCreation.stepThree.modelTitle')).not.toBeInTheDocument()
})
it('should render buttons in correct order (cancel first, then confirm)', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert - Due to flex-row-reverse, confirm appears first visually but cancel is first in DOM
@ -108,25 +91,20 @@ describe('StopEmbeddingModal', () => {
})
it('should render confirm button with primary variant styling', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
expect(confirmButton).toHaveClass('ml-2', 'w-24')
})
it('should render cancel button with default styling', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
expect(cancelButton).toHaveClass('w-24')
})
it('should render all modal elements', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert - Modal should contain title, content, and buttons
@ -137,39 +115,30 @@ describe('StopEmbeddingModal', () => {
})
})
// --------------------------------------------------------------------------
// Props Testing - Test all prop variations
// --------------------------------------------------------------------------
describe('Props', () => {
describe('show prop', () => {
it('should show modal when show is true', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
})
it('should hide modal when show is false', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: false })
// Assert
expect(screen.queryByText('datasetCreation.stepThree.modelTitle')).not.toBeInTheDocument()
})
it('should use default value false when show is not provided', () => {
// Arrange & Act
const onConfirm = vi.fn()
const onHide = vi.fn()
render(<StopEmbeddingModal onConfirm={onConfirm} onHide={onHide} show={false} />)
// Assert
expect(screen.queryByText('datasetCreation.stepThree.modelTitle')).not.toBeInTheDocument()
})
it('should toggle visibility when show prop changes to true', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
@ -193,10 +162,8 @@ describe('StopEmbeddingModal', () => {
describe('onConfirm prop', () => {
it('should accept onConfirm callback function', () => {
// Arrange
const onConfirm = vi.fn()
// Act
renderStopEmbeddingModal({ onConfirm })
// Assert - No errors thrown
@ -206,10 +173,8 @@ describe('StopEmbeddingModal', () => {
describe('onHide prop', () => {
it('should accept onHide callback function', () => {
// Arrange
const onHide = vi.fn()
// Act
renderStopEmbeddingModal({ onHide })
// Assert - No errors thrown
@ -218,51 +183,41 @@ describe('StopEmbeddingModal', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions Tests - Test click events and event handlers
// --------------------------------------------------------------------------
describe('User Interactions', () => {
describe('Confirm Button', () => {
it('should call onConfirm when confirm button is clicked', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onConfirm).toHaveBeenCalledTimes(1)
})
it('should call onHide when confirm button is clicked', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onHide).toHaveBeenCalledTimes(1)
})
it('should call both onConfirm and onHide in correct order when confirm button is clicked', async () => {
// Arrange
const callOrder: string[] = []
const onConfirm = vi.fn(() => callOrder.push('confirm'))
const onHide = vi.fn(() => callOrder.push('hide'))
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
@ -273,12 +228,10 @@ describe('StopEmbeddingModal', () => {
})
it('should handle multiple clicks on confirm button', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
@ -286,7 +239,6 @@ describe('StopEmbeddingModal', () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onConfirm).toHaveBeenCalledTimes(3)
expect(onHide).toHaveBeenCalledTimes(3)
})
@ -294,51 +246,42 @@ describe('StopEmbeddingModal', () => {
describe('Cancel Button', () => {
it('should call onHide when cancel button is clicked', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
await act(async () => {
fireEvent.click(cancelButton)
})
// Assert
expect(onHide).toHaveBeenCalledTimes(1)
})
it('should not call onConfirm when cancel button is clicked', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
await act(async () => {
fireEvent.click(cancelButton)
})
// Assert
expect(onConfirm).not.toHaveBeenCalled()
})
it('should handle multiple clicks on cancel button', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const cancelButton = screen.getByText('datasetCreation.stepThree.modelButtonCancel')
await act(async () => {
fireEvent.click(cancelButton)
fireEvent.click(cancelButton)
})
// Assert
expect(onHide).toHaveBeenCalledTimes(2)
expect(onConfirm).not.toHaveBeenCalled()
})
@ -346,7 +289,6 @@ describe('StopEmbeddingModal', () => {
describe('Close Icon', () => {
it('should call onHide when close span is clicked', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
const { container } = renderStopEmbeddingModal({ onConfirm, onHide })
@ -362,7 +304,6 @@ describe('StopEmbeddingModal', () => {
fireEvent.click(closeSpan)
})
// Assert
expect(onHide).toHaveBeenCalledTimes(1)
}
else {
@ -372,12 +313,10 @@ describe('StopEmbeddingModal', () => {
})
it('should not call onConfirm when close span is clicked', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
const { container } = renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const spans = container.querySelectorAll('span')
const closeSpan = Array.from(spans).find(span =>
span.className && span.getAttribute('class')?.includes('close'),
@ -388,7 +327,6 @@ describe('StopEmbeddingModal', () => {
fireEvent.click(closeSpan)
})
// Assert
expect(onConfirm).not.toHaveBeenCalled()
}
})
@ -396,7 +334,6 @@ describe('StopEmbeddingModal', () => {
describe('Different Close Methods', () => {
it('should distinguish between confirm and cancel actions', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
@ -407,11 +344,9 @@ describe('StopEmbeddingModal', () => {
fireEvent.click(cancelButton)
})
// Assert
expect(onConfirm).not.toHaveBeenCalled()
expect(onHide).toHaveBeenCalledTimes(1)
// Reset
vi.clearAllMocks()
// Act - Click confirm
@ -420,19 +355,15 @@ describe('StopEmbeddingModal', () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onConfirm).toHaveBeenCalledTimes(1)
expect(onHide).toHaveBeenCalledTimes(1)
})
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests - Test null, undefined, empty values and boundaries
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle rapid confirm button clicks', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
@ -444,13 +375,11 @@ describe('StopEmbeddingModal', () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onConfirm).toHaveBeenCalledTimes(10)
expect(onHide).toHaveBeenCalledTimes(10)
})
it('should handle rapid cancel button clicks', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
@ -462,19 +391,16 @@ describe('StopEmbeddingModal', () => {
fireEvent.click(cancelButton)
})
// Assert
expect(onHide).toHaveBeenCalledTimes(10)
expect(onConfirm).not.toHaveBeenCalled()
})
it('should handle callbacks being replaced', async () => {
// Arrange
const onConfirm1 = vi.fn()
const onHide1 = vi.fn()
const onConfirm2 = vi.fn()
const onHide2 = vi.fn()
// Act
const { rerender } = render(
<StopEmbeddingModal show={true} onConfirm={onConfirm1} onHide={onHide1} />,
)
@ -484,7 +410,6 @@ describe('StopEmbeddingModal', () => {
rerender(<StopEmbeddingModal show={true} onConfirm={onConfirm2} onHide={onHide2} />)
})
// Click confirm with new callbacks
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
@ -498,7 +423,6 @@ describe('StopEmbeddingModal', () => {
})
it('should render with all required props', () => {
// Arrange & Act
render(
<StopEmbeddingModal
show={true}
@ -507,50 +431,38 @@ describe('StopEmbeddingModal', () => {
/>,
)
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Layout and Styling Tests - Verify correct structure
// --------------------------------------------------------------------------
describe('Layout and Styling', () => {
it('should have buttons container with flex-row-reverse', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
const buttons = screen.getAllByRole('button')
expect(buttons[0].closest('div')).toHaveClass('flex', 'flex-row-reverse')
})
it('should render title and content elements', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeInTheDocument()
})
it('should render two buttons', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
const buttons = screen.getAllByRole('button')
expect(buttons).toHaveLength(2)
})
})
// --------------------------------------------------------------------------
// submit Function Tests - Test the internal submit function behavior
// --------------------------------------------------------------------------
describe('submit Function', () => {
it('should execute onConfirm first then onHide', async () => {
// Arrange
let confirmTime = 0
let hideTime = 0
let counter = 0
@ -562,73 +474,59 @@ describe('StopEmbeddingModal', () => {
})
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
})
// Assert
expect(confirmTime).toBe(1)
expect(hideTime).toBe(2)
})
it('should call both callbacks exactly once per click', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onConfirm).toHaveBeenCalledTimes(1)
expect(onHide).toHaveBeenCalledTimes(1)
})
it('should pass no arguments to onConfirm', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onConfirm).toHaveBeenCalledWith()
})
it('should pass no arguments to onHide when called from submit', async () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
renderStopEmbeddingModal({ onConfirm, onHide })
// Act
const confirmButton = screen.getByText('datasetCreation.stepThree.modelButtonConfirm')
await act(async () => {
fireEvent.click(confirmButton)
})
// Assert
expect(onHide).toHaveBeenCalledWith()
})
})
// --------------------------------------------------------------------------
// Modal Integration Tests - Verify Modal component integration
// --------------------------------------------------------------------------
describe('Modal Integration', () => {
it('should pass show prop to Modal as isShow', async () => {
// Arrange & Act
const { rerender } = render(
<StopEmbeddingModal show={true} onConfirm={vi.fn()} onHide={vi.fn()} />,
)
@ -648,15 +546,10 @@ describe('StopEmbeddingModal', () => {
})
})
// --------------------------------------------------------------------------
// Accessibility Tests
// --------------------------------------------------------------------------
describe('Accessibility', () => {
it('should have buttons that are focusable', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
const buttons = screen.getAllByRole('button')
buttons.forEach((button) => {
expect(button).not.toHaveAttribute('tabindex', '-1')
@ -664,19 +557,15 @@ describe('StopEmbeddingModal', () => {
})
it('should have semantic button elements', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
const buttons = screen.getAllByRole('button')
expect(buttons).toHaveLength(2)
})
it('should have accessible text content', () => {
// Arrange & Act
renderStopEmbeddingModal({ show: true })
// Assert
expect(screen.getByText('datasetCreation.stepThree.modelTitle')).toBeVisible()
expect(screen.getByText('datasetCreation.stepThree.modelContent')).toBeVisible()
expect(screen.getByText('datasetCreation.stepThree.modelButtonConfirm')).toBeVisible()
@ -684,12 +573,9 @@ describe('StopEmbeddingModal', () => {
})
})
// --------------------------------------------------------------------------
// Component Lifecycle Tests
// --------------------------------------------------------------------------
describe('Component Lifecycle', () => {
it('should unmount cleanly', () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
const { unmount } = renderStopEmbeddingModal({ onConfirm, onHide })
@ -699,12 +585,10 @@ describe('StopEmbeddingModal', () => {
})
it('should not call callbacks after unmount', () => {
// Arrange
const onConfirm = vi.fn()
const onHide = vi.fn()
const { unmount } = renderStopEmbeddingModal({ onConfirm, onHide })
// Act
unmount()
// Assert - No callbacks should be called after unmount
@ -713,7 +597,6 @@ describe('StopEmbeddingModal', () => {
})
it('should re-render correctly when props update', async () => {
// Arrange
const onConfirm1 = vi.fn()
const onHide1 = vi.fn()
const onConfirm2 = vi.fn()

View File

@ -1,6 +1,6 @@
import type { TopBarProps } from './index'
import type { TopBarProps } from '../index'
import { render, screen } from '@testing-library/react'
import { TopBar } from './index'
import { TopBar } from '../index'
// Mock next/link to capture href values
vi.mock('next/link', () => ({
@ -23,31 +23,23 @@ const renderTopBar = (props: Partial<TopBarProps> = {}) => {
}
}
// ============================================================================
// TopBar Component Tests
// ============================================================================
describe('TopBar', () => {
beforeEach(() => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests - Verify component renders properly
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange & Act
renderTopBar()
// Assert
expect(screen.getByTestId('back-link')).toBeInTheDocument()
})
it('should render back link with arrow icon', () => {
// Arrange & Act
const { container } = renderTopBar()
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toBeInTheDocument()
// Check for the arrow icon (svg element)
@ -56,15 +48,12 @@ describe('TopBar', () => {
})
it('should render fallback route text', () => {
// Arrange & Act
renderTopBar()
// Assert
expect(screen.getByText('datasetCreation.steps.header.fallbackRoute')).toBeInTheDocument()
})
it('should render Stepper component with 3 steps', () => {
// Arrange & Act
renderTopBar({ activeIndex: 0 })
// Assert - Check for step translations
@ -74,10 +63,8 @@ describe('TopBar', () => {
})
it('should apply default container classes', () => {
// Arrange & Act
const { container } = renderTopBar()
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('relative')
expect(wrapper).toHaveClass('flex')
@ -90,25 +77,19 @@ describe('TopBar', () => {
})
})
// --------------------------------------------------------------------------
// Props Testing - Test all prop variations
// --------------------------------------------------------------------------
describe('Props', () => {
describe('className prop', () => {
it('should apply custom className when provided', () => {
// Arrange & Act
const { container } = renderTopBar({ className: 'custom-class' })
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-class')
})
it('should merge custom className with default classes', () => {
// Arrange & Act
const { container } = renderTopBar({ className: 'my-custom-class another-class' })
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('relative')
expect(wrapper).toHaveClass('flex')
@ -117,20 +98,16 @@ describe('TopBar', () => {
})
it('should render correctly without className', () => {
// Arrange & Act
const { container } = renderTopBar({ className: undefined })
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('relative')
expect(wrapper).toHaveClass('flex')
})
it('should handle empty string className', () => {
// Arrange & Act
const { container } = renderTopBar({ className: '' })
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('relative')
})
@ -138,34 +115,27 @@ describe('TopBar', () => {
describe('datasetId prop', () => {
it('should set fallback route to /datasets when datasetId is undefined', () => {
// Arrange & Act
renderTopBar({ datasetId: undefined })
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveAttribute('href', '/datasets')
})
it('should set fallback route to /datasets/:id/documents when datasetId is provided', () => {
// Arrange & Act
renderTopBar({ datasetId: 'dataset-123' })
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveAttribute('href', '/datasets/dataset-123/documents')
})
it('should handle various datasetId formats', () => {
// Arrange & Act
renderTopBar({ datasetId: 'abc-def-ghi-123' })
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveAttribute('href', '/datasets/abc-def-ghi-123/documents')
})
it('should handle empty string datasetId', () => {
// Arrange & Act
renderTopBar({ datasetId: '' })
// Assert - Empty string is falsy, so fallback to /datasets
@ -176,7 +146,6 @@ describe('TopBar', () => {
describe('activeIndex prop', () => {
it('should pass activeIndex to Stepper component (index 0)', () => {
// Arrange & Act
const { container } = renderTopBar({ activeIndex: 0 })
// Assert - First step should be active (has specific styling)
@ -185,7 +154,6 @@ describe('TopBar', () => {
})
it('should pass activeIndex to Stepper component (index 1)', () => {
// Arrange & Act
renderTopBar({ activeIndex: 1 })
// Assert - Stepper is rendered with correct props
@ -194,15 +162,12 @@ describe('TopBar', () => {
})
it('should pass activeIndex to Stepper component (index 2)', () => {
// Arrange & Act
renderTopBar({ activeIndex: 2 })
// Assert
expect(screen.getByText('datasetCreation.steps.three')).toBeInTheDocument()
})
it('should handle edge case activeIndex of -1', () => {
// Arrange & Act
const { container } = renderTopBar({ activeIndex: -1 })
// Assert - Component should render without crashing
@ -210,7 +175,6 @@ describe('TopBar', () => {
})
it('should handle edge case activeIndex beyond steps length', () => {
// Arrange & Act
const { container } = renderTopBar({ activeIndex: 10 })
// Assert - Component should render without crashing
@ -219,15 +183,12 @@ describe('TopBar', () => {
})
})
// --------------------------------------------------------------------------
// Memoization Tests - Test useMemo logic and dependencies
// --------------------------------------------------------------------------
describe('Memoization Logic', () => {
it('should compute fallbackRoute based on datasetId', () => {
// Arrange & Act - With datasetId
const { rerender } = render(<TopBar activeIndex={0} datasetId="test-id" />)
// Assert
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/test-id/documents')
// Act - Rerender with different datasetId
@ -238,35 +199,27 @@ describe('TopBar', () => {
})
it('should update fallbackRoute when datasetId changes from undefined to defined', () => {
// Arrange
const { rerender } = render(<TopBar activeIndex={0} />)
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets')
// Act
rerender(<TopBar activeIndex={0} datasetId="new-dataset" />)
// Assert
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/new-dataset/documents')
})
it('should update fallbackRoute when datasetId changes from defined to undefined', () => {
// Arrange
const { rerender } = render(<TopBar activeIndex={0} datasetId="existing-id" />)
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/existing-id/documents')
// Act
rerender(<TopBar activeIndex={0} datasetId={undefined} />)
// Assert
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets')
})
it('should not change fallbackRoute when activeIndex changes but datasetId stays same', () => {
// Arrange
const { rerender } = render(<TopBar activeIndex={0} datasetId="stable-id" />)
const initialHref = screen.getByTestId('back-link').getAttribute('href')
// Act
rerender(<TopBar activeIndex={1} datasetId="stable-id" />)
// Assert - href should remain the same
@ -274,11 +227,9 @@ describe('TopBar', () => {
})
it('should not change fallbackRoute when className changes but datasetId stays same', () => {
// Arrange
const { rerender } = render(<TopBar activeIndex={0} datasetId="stable-id" className="class-1" />)
const initialHref = screen.getByTestId('back-link').getAttribute('href')
// Act
rerender(<TopBar activeIndex={0} datasetId="stable-id" className="class-2" />)
// Assert - href should remain the same
@ -286,24 +237,18 @@ describe('TopBar', () => {
})
})
// --------------------------------------------------------------------------
// Link Component Tests
// --------------------------------------------------------------------------
describe('Link Component', () => {
it('should render Link with replace prop', () => {
// Arrange & Act
renderTopBar()
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveAttribute('data-replace', 'true')
})
it('should render Link with correct classes', () => {
// Arrange & Act
renderTopBar()
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveClass('inline-flex')
expect(backLink).toHaveClass('h-12')
@ -316,84 +261,63 @@ describe('TopBar', () => {
})
})
// --------------------------------------------------------------------------
// STEP_T_MAP Tests - Verify step translations
// --------------------------------------------------------------------------
describe('STEP_T_MAP Translations', () => {
it('should render step one translation', () => {
// Arrange & Act
renderTopBar({ activeIndex: 0 })
// Assert
expect(screen.getByText('datasetCreation.steps.one')).toBeInTheDocument()
})
it('should render step two translation', () => {
// Arrange & Act
renderTopBar({ activeIndex: 1 })
// Assert
expect(screen.getByText('datasetCreation.steps.two')).toBeInTheDocument()
})
it('should render step three translation', () => {
// Arrange & Act
renderTopBar({ activeIndex: 2 })
// Assert
expect(screen.getByText('datasetCreation.steps.three')).toBeInTheDocument()
})
it('should render all three step translations', () => {
// Arrange & Act
renderTopBar({ activeIndex: 0 })
// Assert
expect(screen.getByText('datasetCreation.steps.one')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.steps.two')).toBeInTheDocument()
expect(screen.getByText('datasetCreation.steps.three')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Edge Cases and Error Handling Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle special characters in datasetId', () => {
// Arrange & Act
renderTopBar({ datasetId: 'dataset-with-special_chars.123' })
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveAttribute('href', '/datasets/dataset-with-special_chars.123/documents')
})
it('should handle very long datasetId', () => {
// Arrange
const longId = 'a'.repeat(100)
// Act
renderTopBar({ datasetId: longId })
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveAttribute('href', `/datasets/${longId}/documents`)
})
it('should handle UUID format datasetId', () => {
// Arrange
const uuid = '550e8400-e29b-41d4-a716-446655440000'
// Act
renderTopBar({ datasetId: uuid })
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toHaveAttribute('href', `/datasets/${uuid}/documents`)
})
it('should handle whitespace in className', () => {
// Arrange & Act
const { container } = renderTopBar({ className: ' spaced-class ' })
// Assert - classNames utility handles whitespace
@ -402,35 +326,28 @@ describe('TopBar', () => {
})
it('should render correctly with all props provided', () => {
// Arrange & Act
const { container } = renderTopBar({
className: 'custom-class',
datasetId: 'full-props-id',
activeIndex: 2,
})
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('custom-class')
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets/full-props-id/documents')
})
it('should render correctly with minimal props (only activeIndex)', () => {
// Arrange & Act
const { container } = renderTopBar({ activeIndex: 0 })
// Assert
expect(container.firstChild).toBeInTheDocument()
expect(screen.getByTestId('back-link')).toHaveAttribute('href', '/datasets')
})
})
// --------------------------------------------------------------------------
// Stepper Integration Tests
// --------------------------------------------------------------------------
describe('Stepper Integration', () => {
it('should pass steps array with correct structure to Stepper', () => {
// Arrange & Act
renderTopBar({ activeIndex: 0 })
// Assert - All step names should be rendered
@ -444,7 +361,6 @@ describe('TopBar', () => {
})
it('should render Stepper in centered position', () => {
// Arrange & Act
const { container } = renderTopBar({ activeIndex: 0 })
// Assert - Check for centered positioning classes
@ -453,7 +369,6 @@ describe('TopBar', () => {
})
it('should render step dividers between steps', () => {
// Arrange & Act
const { container } = renderTopBar({ activeIndex: 0 })
// Assert - Check for dividers (h-px w-4 bg-divider-deep)
@ -462,15 +377,10 @@ describe('TopBar', () => {
})
})
// --------------------------------------------------------------------------
// Accessibility Tests
// --------------------------------------------------------------------------
describe('Accessibility', () => {
it('should have accessible back link', () => {
// Arrange & Act
renderTopBar()
// Assert
const backLink = screen.getByTestId('back-link')
expect(backLink).toBeInTheDocument()
// Link should have visible text
@ -478,7 +388,6 @@ describe('TopBar', () => {
})
it('should have visible arrow icon in back link', () => {
// Arrange & Act
const { container } = renderTopBar()
// Assert - Arrow icon should be visible
@ -488,12 +397,9 @@ describe('TopBar', () => {
})
})
// --------------------------------------------------------------------------
// Re-render Tests
// --------------------------------------------------------------------------
describe('Re-render Behavior', () => {
it('should update activeIndex on re-render', () => {
// Arrange
const { rerender, container } = render(<TopBar activeIndex={0} />)
// Initial check
@ -507,21 +413,17 @@ describe('TopBar', () => {
})
it('should update className on re-render', () => {
// Arrange
const { rerender, container } = render(<TopBar activeIndex={0} className="initial-class" />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('initial-class')
// Act
rerender(<TopBar activeIndex={0} className="updated-class" />)
// Assert
expect(wrapper).toHaveClass('updated-class')
expect(wrapper).not.toHaveClass('initial-class')
})
it('should handle multiple rapid re-renders', () => {
// Arrange
const { rerender, container } = render(<TopBar activeIndex={0} />)
// Act - Multiple rapid re-renders

View File

@ -1,14 +1,10 @@
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import CrawledResult from './base/crawled-result'
import CrawledResultItem from './base/crawled-result-item'
import Header from './base/header'
import Input from './base/input'
// ============================================================================
// Test Data Factories
// ============================================================================
import CrawledResult from '../base/crawled-result'
import CrawledResultItem from '../base/crawled-result-item'
import Header from '../base/header'
import Input from '../base/input'
const createCrawlResultItem = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
title: 'Test Page Title',
@ -18,9 +14,7 @@ const createCrawlResultItem = (overrides: Partial<CrawlResultItem> = {}): CrawlR
...overrides,
})
// ============================================================================
// Input Component Tests
// ============================================================================
describe('Input', () => {
beforeEach(() => {
@ -155,9 +149,7 @@ describe('Input', () => {
})
})
// ============================================================================
// Header Component Tests
// ============================================================================
describe('Header', () => {
const createHeaderProps = (overrides: Partial<Parameters<typeof Header>[0]> = {}) => ({
@ -254,9 +246,7 @@ describe('Header', () => {
})
})
// ============================================================================
// CrawledResultItem Component Tests
// ============================================================================
describe('CrawledResultItem', () => {
const createItemProps = (overrides: Partial<Parameters<typeof CrawledResultItem>[0]> = {}) => ({
@ -359,9 +349,7 @@ describe('CrawledResultItem', () => {
})
})
// ============================================================================
// CrawledResult Component Tests
// ============================================================================
describe('CrawledResult', () => {
const createResultProps = (overrides: Partial<Parameters<typeof CrawledResult>[0]> = {}) => ({
@ -487,7 +475,6 @@ describe('CrawledResult', () => {
const props = createResultProps({ list, checkedList: [list[0], list[1]], onSelectedChange })
render(<CrawledResult {...props} />)
// Click the first item's checkbox to uncheck it
await userEvent.click(getItemCheckbox(0))
expect(onSelectedChange).toHaveBeenCalledWith([list[1]])
@ -505,7 +492,6 @@ describe('CrawledResult', () => {
render(<CrawledResult {...props} />)
// Click preview on second item
const previewButtons = screen.getAllByText('datasetCreation.stepOne.website.preview')
await userEvent.click(previewButtons[1])
@ -522,7 +508,6 @@ describe('CrawledResult', () => {
render(<CrawledResult {...props} />)
// Click preview on first item
const previewButtons = screen.getAllByText('datasetCreation.stepOne.website.preview')
await userEvent.click(previewButtons[0])

View File

@ -3,7 +3,7 @@ import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
import Website from './index'
import Website from '../index'
const mockSetShowAccountSettingModal = vi.fn()
@ -13,26 +13,26 @@ vi.mock('@/context/modal-context', () => ({
}),
}))
vi.mock('./index.module.css', () => ({
vi.mock('../index.module.css', () => ({
default: {
jinaLogo: 'jina-logo',
watercrawlLogo: 'watercrawl-logo',
},
}))
vi.mock('./firecrawl', () => ({
vi.mock('../firecrawl', () => ({
default: (props: Record<string, unknown>) => <div data-testid="firecrawl-component" data-props={JSON.stringify(props)} />,
}))
vi.mock('./jina-reader', () => ({
vi.mock('../jina-reader', () => ({
default: (props: Record<string, unknown>) => <div data-testid="jina-reader-component" data-props={JSON.stringify(props)} />,
}))
vi.mock('./watercrawl', () => ({
vi.mock('../watercrawl', () => ({
default: (props: Record<string, unknown>) => <div data-testid="watercrawl-component" data-props={JSON.stringify(props)} />,
}))
vi.mock('./no-data', () => ({
vi.mock('../no-data', () => ({
default: ({ onConfig, provider }: { onConfig: () => void, provider: string }) => (
<div data-testid="no-data-component" data-provider={provider}>
<button onClick={onConfig} data-testid="no-data-config-button">Configure</button>

View File

@ -1,14 +1,12 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { DataSourceProvider } from '@/models/common'
import NoData from './no-data'
import NoData from '../no-data'
// ============================================================================
// Mock Setup
// ============================================================================
// Mock CSS module
vi.mock('./index.module.css', () => ({
vi.mock('../index.module.css', () => ({
default: {
jinaLogo: 'jinaLogo',
watercrawlLogo: 'watercrawlLogo',
@ -26,9 +24,7 @@ vi.mock('@/config', () => ({
get ENABLE_WEBSITE_WATERCRAWL() { return mockEnableWaterCrawl },
}))
// ============================================================================
// NoData Component Tests
// ============================================================================
describe('NoData', () => {
const mockOnConfig = vi.fn()
@ -40,95 +36,70 @@ describe('NoData', () => {
mockEnableWaterCrawl = true
})
// --------------------------------------------------------------------------
// Rendering Tests - Per Provider
// --------------------------------------------------------------------------
describe('Rendering per provider', () => {
it('should render fireCrawl provider with emoji and not-configured message', () => {
// Arrange & Act
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
// Assert
expect(screen.getByText('🔥')).toBeInTheDocument()
const titleAndDesc = screen.getAllByText(/fireCrawlNotConfigured/i)
expect(titleAndDesc).toHaveLength(2)
})
it('should render jinaReader provider with jina logo and not-configured message', () => {
// Arrange & Act
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />)
// Assert
const titleAndDesc = screen.getAllByText(/jinaReaderNotConfigured/i)
expect(titleAndDesc).toHaveLength(2)
})
it('should render waterCrawl provider with emoji and not-configured message', () => {
// Arrange & Act
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.waterCrawl} />)
// Assert
expect(screen.getByText('💧')).toBeInTheDocument()
const titleAndDesc = screen.getAllByText(/waterCrawlNotConfigured/i)
expect(titleAndDesc).toHaveLength(2)
})
it('should render configure button for each provider', () => {
// Arrange & Act
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
// Assert
expect(screen.getByRole('button', { name: /configure/i })).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// User Interactions
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onConfig when configure button is clicked', () => {
// Arrange
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /configure/i }))
// Assert
expect(mockOnConfig).toHaveBeenCalledTimes(1)
})
it('should call onConfig for jinaReader provider', () => {
// Arrange
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /configure/i }))
// Assert
expect(mockOnConfig).toHaveBeenCalledTimes(1)
})
it('should call onConfig for waterCrawl provider', () => {
// Arrange
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.waterCrawl} />)
// Act
fireEvent.click(screen.getByRole('button', { name: /configure/i }))
// Assert
expect(mockOnConfig).toHaveBeenCalledTimes(1)
})
})
// --------------------------------------------------------------------------
// Feature Flag Disabled - Returns null
// --------------------------------------------------------------------------
describe('Disabled providers (feature flag off)', () => {
it('should fall back to jinaReader when fireCrawl is disabled but jinaReader enabled', () => {
// Arrange — fireCrawl config is null, falls back to providerConfig.jinareader
mockEnableFirecrawl = false
// Act
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />,
)
@ -142,12 +113,10 @@ describe('NoData', () => {
// Arrange — jinaReader is the only provider without a fallback
mockEnableJinaReader = false
// Act
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />,
)
// Assert
expect(container.innerHTML).toBe('')
})
@ -155,7 +124,6 @@ describe('NoData', () => {
// Arrange — waterCrawl config is null, falls back to providerConfig.jinareader
mockEnableWaterCrawl = false
// Act
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.waterCrawl} />,
)
@ -166,9 +134,7 @@ describe('NoData', () => {
})
})
// --------------------------------------------------------------------------
// Fallback behavior
// --------------------------------------------------------------------------
describe('Fallback behavior', () => {
it('should fall back to jinaReader config for unknown provider value', () => {
// Arrange - the || fallback goes to providerConfig.jinareader
@ -176,30 +142,22 @@ describe('NoData', () => {
// by checking that jinaReader is the fallback when provider doesn't match
mockEnableJinaReader = true
// Act
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.jinaReader} />)
// Assert
expect(screen.getAllByText(/jinaReaderNotConfigured/i).length).toBeGreaterThan(0)
})
})
// --------------------------------------------------------------------------
// Edge Cases
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should not call onConfig without user interaction', () => {
// Arrange & Act
render(<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />)
// Assert
expect(mockOnConfig).not.toHaveBeenCalled()
})
it('should render correctly when all providers are enabled', () => {
// Arrange - all flags are true by default
// Act
const { rerender } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />,
)
@ -213,17 +171,14 @@ describe('NoData', () => {
})
it('should return null when all providers are disabled and fireCrawl is selected', () => {
// Arrange
mockEnableFirecrawl = false
mockEnableJinaReader = false
mockEnableWaterCrawl = false
// Act
const { container } = render(
<NoData onConfig={mockOnConfig} provider={DataSourceProvider.fireCrawl} />,
)
// Assert
expect(container.innerHTML).toBe('')
})
})

View File

@ -1,14 +1,12 @@
import type { CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import WebsitePreview from './preview'
import WebsitePreview from '../preview'
// ============================================================================
// Mock Setup
// ============================================================================
// Mock the CSS module import - returns class names as-is
vi.mock('../file-preview/index.module.css', () => ({
vi.mock('../../file-preview/index.module.css', () => ({
default: {
filePreview: 'filePreview',
previewHeader: 'previewHeader',
@ -18,9 +16,7 @@ vi.mock('../file-preview/index.module.css', () => ({
},
}))
// ============================================================================
// Test Data Factory
// ============================================================================
const createPayload = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
title: 'Test Page Title',
@ -30,9 +26,7 @@ const createPayload = (overrides: Partial<CrawlResultItem> = {}): CrawlResultIte
...overrides,
})
// ============================================================================
// WebsitePreview Component Tests
// ============================================================================
describe('WebsitePreview', () => {
const mockHidePreview = vi.fn()
@ -41,26 +35,18 @@ describe('WebsitePreview', () => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
// Arrange
const payload = createPayload()
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
})
it('should render the page preview header text', () => {
// Arrange
const payload = createPayload()
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - i18n returns the key path
@ -68,45 +54,34 @@ describe('WebsitePreview', () => {
})
it('should render the payload title', () => {
// Arrange
const payload = createPayload({ title: 'My Custom Page' })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
expect(screen.getByText('My Custom Page')).toBeInTheDocument()
})
it('should render the payload source_url', () => {
// Arrange
const payload = createPayload({ source_url: 'https://docs.dify.ai/intro' })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
const urlElement = screen.getByText('https://docs.dify.ai/intro')
expect(urlElement).toBeInTheDocument()
expect(urlElement).toHaveAttribute('title', 'https://docs.dify.ai/intro')
})
it('should render the payload markdown content', () => {
// Arrange
const payload = createPayload({ markdown: 'Hello world markdown' })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
expect(screen.getByText('Hello world markdown')).toBeInTheDocument()
})
it('should render the close button (XMarkIcon)', () => {
// Arrange
const payload = createPayload()
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - the close button container is a div with cursor-pointer
@ -115,12 +90,8 @@ describe('WebsitePreview', () => {
})
})
// --------------------------------------------------------------------------
// User Interactions
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call hidePreview when close button is clicked', () => {
// Arrange
const payload = createPayload()
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
@ -130,58 +101,44 @@ describe('WebsitePreview', () => {
.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
// Assert
expect(mockHidePreview).toHaveBeenCalledTimes(1)
})
it('should call hidePreview exactly once per click', () => {
// Arrange
const payload = createPayload()
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Act
const closeButton = screen.getByText(/pagePreview/i)
.closest('[class*="title"]')!
.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
fireEvent.click(closeButton)
// Assert
expect(mockHidePreview).toHaveBeenCalledTimes(2)
})
})
// --------------------------------------------------------------------------
// Props Display Tests
// --------------------------------------------------------------------------
describe('Props Display', () => {
it('should display all payload fields simultaneously', () => {
// Arrange
const payload = createPayload({
title: 'Full Title',
source_url: 'https://full.example.com',
markdown: 'Full markdown text',
})
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
expect(screen.getByText('Full Title')).toBeInTheDocument()
expect(screen.getByText('https://full.example.com')).toBeInTheDocument()
expect(screen.getByText('Full markdown text')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Edge Cases
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should render with empty title', () => {
// Arrange
const payload = createPayload({ title: '' })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - component still renders, url is visible
@ -189,44 +146,33 @@ describe('WebsitePreview', () => {
})
it('should render with empty markdown', () => {
// Arrange
const payload = createPayload({ markdown: '' })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
})
it('should render with empty source_url', () => {
// Arrange
const payload = createPayload({ source_url: '' })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
expect(screen.getByText('Test Page Title')).toBeInTheDocument()
})
it('should render with very long content', () => {
// Arrange
const longMarkdown = 'A'.repeat(5000)
const payload = createPayload({ markdown: longMarkdown })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert
expect(screen.getByText(longMarkdown)).toBeInTheDocument()
})
it('should render with special characters in title', () => {
// Arrange
const payload = createPayload({ title: '<script>alert("xss")</script>' })
// Act
render(<WebsitePreview payload={payload} hidePreview={mockHidePreview} />)
// Assert - React escapes HTML by default
@ -234,20 +180,15 @@ describe('WebsitePreview', () => {
})
})
// --------------------------------------------------------------------------
// CSS Module Classes
// --------------------------------------------------------------------------
describe('CSS Module Classes', () => {
it('should apply filePreview class to root container', () => {
// Arrange
const payload = createPayload()
// Act
const { container } = render(
<WebsitePreview payload={payload} hidePreview={mockHidePreview} />,
)
// Assert
const root = container.firstElementChild
expect(root?.className).toContain('filePreview')
expect(root?.className).toContain('h-full')

View File

@ -1,6 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import CheckboxWithLabel from './checkbox-with-label'
import CheckboxWithLabel from '../checkbox-with-label'
vi.mock('@/app/components/base/tooltip', () => ({
default: ({ popupContent }: { popupContent?: React.ReactNode }) => <div data-testid="tooltip">{popupContent}</div>,

View File

@ -1,11 +1,7 @@
import type { CrawlResultItem as CrawlResultItemType } from '@/models/datasets'
import { fireEvent, 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 }),
}))
import CrawledResultItem from '../crawled-result-item'
describe('CrawledResultItem', () => {
const defaultProps = {

Some files were not shown because too many files have changed in this diff Show More