Files
dify/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/index.spec.tsx
CodingOnStar dae130d8d2 refactor(tests): improve mock document creation and enhance test stability
- Updated the  function to use a deterministic counter for IDs, reducing flakiness in tests.
- Ensured the ID counter resets before each test to maintain reproducibility.
- Adjusted assertions in the  tests to explicitly check for error handling when the documents array is empty, improving clarity and reliability of edge case testing.
- Fixed a link in the  tests to ensure it points to the correct documentation URL.

These changes enhance the robustness and reliability of the testing suite for document processing components.
2025-12-18 18:50:21 +08:00

1261 lines
41 KiB
TypeScript

import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import EmbeddingProcess from './index'
import type { DocumentIndexingStatus, IndexingStatusResponse } from '@/models/datasets'
import { DatasourceType, type InitialDocumentDetail } from '@/models/pipeline'
import { Plan } from '@/app/components/billing/type'
import { RETRIEVE_METHOD } from '@/types/app'
import { IndexingType } from '@/app/components/datasets/create/step-two'
// ==========================================
// Mock External Dependencies
// ==========================================
// Mock next/navigation
const mockPush = jest.fn()
jest.mock('next/navigation', () => ({
useRouter: () => ({
push: mockPush,
}),
}))
// Mock next/link
jest.mock('next/link', () => {
return function MockLink({ children, href, ...props }: { children: React.ReactNode; href: string }) {
return <a href={href} {...props}>{children}</a>
}
})
// Mock provider context
let mockEnableBilling = false
let mockPlanType: Plan = Plan.sandbox
jest.mock('@/context/provider-context', () => ({
useProviderContext: () => ({
enableBilling: mockEnableBilling,
plan: { type: mockPlanType },
}),
}))
// Mock useIndexingStatusBatch hook
let mockFetchIndexingStatus: jest.Mock
let mockIndexingStatusData: IndexingStatusResponse[] = []
jest.mock('@/service/knowledge/use-dataset', () => ({
useIndexingStatusBatch: () => ({
mutateAsync: mockFetchIndexingStatus,
}),
useProcessRule: () => ({
data: {
mode: 'custom',
rules: { parent_mode: 'paragraph' },
},
}),
}))
// Mock useInvalidDocumentList hook
const mockInvalidDocumentList = jest.fn()
jest.mock('@/service/knowledge/use-document', () => ({
useInvalidDocumentList: () => mockInvalidDocumentList,
}))
// Mock useDatasetApiAccessUrl hook
jest.mock('@/hooks/use-api-access-url', () => ({
useDatasetApiAccessUrl: () => 'https://docs.dify.ai/api-reference/datasets',
}))
// ==========================================
// Test Data Factory Functions
// ==========================================
/**
* Creates a mock InitialDocumentDetail for testing
* Uses deterministic counter-based IDs to avoid flaky tests
*/
let documentIdCounter = 0
const createMockDocument = (overrides: Partial<InitialDocumentDetail> = {}): InitialDocumentDetail => ({
id: overrides.id ?? `doc-${++documentIdCounter}`,
name: 'test-document.txt',
data_source_type: DatasourceType.localFile,
data_source_info: {},
enable: true,
error: '',
indexing_status: 'waiting' as DocumentIndexingStatus,
position: 0,
...overrides,
})
/**
* Creates a mock IndexingStatusResponse for testing
*/
const createMockIndexingStatus = (overrides: Partial<IndexingStatusResponse> = {}): IndexingStatusResponse => ({
id: `doc-${Math.random().toString(36).slice(2, 9)}`,
indexing_status: 'waiting' as DocumentIndexingStatus,
processing_started_at: Date.now(),
parsing_completed_at: 0,
cleaning_completed_at: 0,
splitting_completed_at: 0,
completed_at: null,
paused_at: null,
error: null,
stopped_at: null,
completed_segments: 0,
total_segments: 100,
...overrides,
})
/**
* Creates default props for EmbeddingProcess component
*/
const createDefaultProps = (overrides: Partial<{
datasetId: string
batchId: string
documents: InitialDocumentDetail[]
indexingType: IndexingType
retrievalMethod: RETRIEVE_METHOD
}> = {}) => ({
datasetId: 'dataset-123',
batchId: 'batch-456',
documents: [createMockDocument({ id: 'doc-1', name: 'test-doc.pdf' })],
indexingType: IndexingType.QUALIFIED,
retrievalMethod: RETRIEVE_METHOD.semantic,
...overrides,
})
// ==========================================
// Test Suite
// ==========================================
describe('EmbeddingProcess', () => {
beforeEach(() => {
jest.clearAllMocks()
jest.useFakeTimers()
// Reset deterministic ID counter for reproducible tests
documentIdCounter = 0
// Reset mock states
mockEnableBilling = false
mockPlanType = Plan.sandbox
mockIndexingStatusData = []
// Setup default mock for fetchIndexingStatus
mockFetchIndexingStatus = jest.fn().mockImplementation((_, options) => {
options?.onSuccess?.({ data: mockIndexingStatusData })
options?.onSettled?.()
return Promise.resolve({ data: mockIndexingStatusData })
})
})
afterEach(() => {
jest.useRealTimers()
})
// ==========================================
// Rendering Tests
// ==========================================
describe('Rendering', () => {
// Tests basic rendering functionality
it('should render without crashing', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert
expect(screen.getByTestId('rule-detail')).toBeInTheDocument()
})
it('should render RuleDetail component with correct props', () => {
// Arrange
const props = createDefaultProps({
indexingType: IndexingType.ECONOMICAL,
retrievalMethod: RETRIEVE_METHOD.fullText,
})
// Act
render(<EmbeddingProcess {...props} />)
// Assert - RuleDetail renders FieldInfo components with translated text
// Check that the component renders without error
expect(screen.getByTestId('rule-detail')).toBeInTheDocument()
})
it('should render API reference link with correct URL', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert
const apiLink = screen.getByRole('link', { name: /access the api/i })
expect(apiLink).toHaveAttribute('href', 'https://docs.dify.ai/api-reference/datasets')
expect(apiLink).toHaveAttribute('target', '_blank')
expect(apiLink).toHaveAttribute('rel', 'noopener noreferrer')
})
it('should render navigation button', () => {
// Arrange
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert
expect(screen.getByText('datasetCreation.stepThree.navTo')).toBeInTheDocument()
})
})
// ==========================================
// Billing/Upgrade Banner Tests
// ==========================================
describe('Billing and Upgrade Banner', () => {
// Tests for billing-related UI
it('should not show upgrade banner when billing is disabled', () => {
// Arrange
mockEnableBilling = false
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert
expect(screen.queryByText('billing.plansCommon.documentProcessingPriorityUpgrade')).not.toBeInTheDocument()
})
it('should show upgrade banner when billing is enabled and plan is not team', () => {
// Arrange
mockEnableBilling = true
mockPlanType = Plan.sandbox
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert
expect(screen.getByText('billing.plansCommon.documentProcessingPriorityUpgrade')).toBeInTheDocument()
})
it('should not show upgrade banner when plan is team', () => {
// Arrange
mockEnableBilling = true
mockPlanType = Plan.team
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert
expect(screen.queryByText('billing.plansCommon.documentProcessingPriorityUpgrade')).not.toBeInTheDocument()
})
it('should show upgrade banner for professional plan', () => {
// Arrange
mockEnableBilling = true
mockPlanType = Plan.professional
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert
expect(screen.getByText('billing.plansCommon.documentProcessingPriorityUpgrade')).toBeInTheDocument()
})
})
// ==========================================
// Status Display Tests
// ==========================================
describe('Status Display', () => {
// Tests for embedding status display
it('should show waiting status when all documents are waiting', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'waiting' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.waiting')).toBeInTheDocument()
})
it('should show processing status when any document is indexing', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.processing')).toBeInTheDocument()
})
it('should show processing status when any document is splitting', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'splitting' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.processing')).toBeInTheDocument()
})
it('should show processing status when any document is parsing', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'parsing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.processing')).toBeInTheDocument()
})
it('should show processing status when any document is cleaning', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'cleaning' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.processing')).toBeInTheDocument()
})
it('should show completed status when all documents are completed', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'completed' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.completed')).toBeInTheDocument()
})
it('should show completed status when all documents have error status', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'error', error: 'Processing failed' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.completed')).toBeInTheDocument()
})
it('should show completed status when all documents are paused', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'paused' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.completed')).toBeInTheDocument()
})
})
// ==========================================
// Progress Bar Tests
// ==========================================
describe('Progress Display', () => {
// Tests for progress bar rendering
it('should show progress percentage for embedding documents', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: 'indexing',
completed_segments: 50,
total_segments: 100,
}),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('50%')).toBeInTheDocument()
})
it('should cap progress at 100%', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: 'indexing',
completed_segments: 150,
total_segments: 100,
}),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('100%')).toBeInTheDocument()
})
it('should show 0% when total_segments is 0', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: 'indexing',
completed_segments: 0,
total_segments: 0,
}),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('0%')).toBeInTheDocument()
})
it('should not show progress for completed documents', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: 'completed',
completed_segments: 100,
total_segments: 100,
}),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.queryByText('100%')).not.toBeInTheDocument()
})
})
// ==========================================
// Polling Logic Tests
// ==========================================
describe('Polling Logic', () => {
// Tests for API polling behavior
it('should start polling on mount', async () => {
// Arrange
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
// Assert - verify fetch was called at least once
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
})
it('should continue polling while documents are processing', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
const initialCallCount = mockFetchIndexingStatus.mock.calls.length
// Act
render(<EmbeddingProcess {...props} />)
// Wait for initial fetch
await waitFor(() => {
expect(mockFetchIndexingStatus.mock.calls.length).toBeGreaterThan(initialCallCount)
})
const afterInitialCount = mockFetchIndexingStatus.mock.calls.length
// Advance timer for next poll
jest.advanceTimersByTime(2500)
// Assert - should poll again
await waitFor(() => {
expect(mockFetchIndexingStatus.mock.calls.length).toBeGreaterThan(afterInitialCount)
})
})
it('should stop polling when all documents are completed', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'completed' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
// Wait for initial fetch and state update
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
const callCountAfterComplete = mockFetchIndexingStatus.mock.calls.length
// Advance timer - polling should have stopped
jest.advanceTimersByTime(5000)
// Assert - call count should not increase significantly after completion
// Note: Due to React Strict Mode, there might be double renders
expect(mockFetchIndexingStatus.mock.calls.length).toBeLessThanOrEqual(callCountAfterComplete + 1)
})
it('should stop polling when all documents have errors', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'error' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
// Wait for initial fetch
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
const callCountAfterError = mockFetchIndexingStatus.mock.calls.length
// Advance timer
jest.advanceTimersByTime(5000)
// Assert - should not poll significantly more after error state
expect(mockFetchIndexingStatus.mock.calls.length).toBeLessThanOrEqual(callCountAfterError + 1)
})
it('should stop polling when all documents are paused', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'paused' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
// Wait for initial fetch
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
const callCountAfterPaused = mockFetchIndexingStatus.mock.calls.length
// Advance timer
jest.advanceTimersByTime(5000)
// Assert - should not poll significantly more after paused state
expect(mockFetchIndexingStatus.mock.calls.length).toBeLessThanOrEqual(callCountAfterPaused + 1)
})
it('should cleanup timeout on unmount', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
const { unmount } = render(<EmbeddingProcess {...props} />)
// Wait for initial fetch
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
const callCountBeforeUnmount = mockFetchIndexingStatus.mock.calls.length
// Unmount before next poll
unmount()
// Advance timer
jest.advanceTimersByTime(5000)
// Assert - should not poll after unmount
expect(mockFetchIndexingStatus.mock.calls.length).toBe(callCountBeforeUnmount)
})
})
// ==========================================
// User Interactions Tests
// ==========================================
describe('User Interactions', () => {
// Tests for button clicks and navigation
it('should navigate to document list when nav button is clicked', async () => {
// Arrange
const props = createDefaultProps({ datasetId: 'my-dataset-123' })
// Act
render(<EmbeddingProcess {...props} />)
const navButton = screen.getByText('datasetCreation.stepThree.navTo')
fireEvent.click(navButton)
// Assert
expect(mockInvalidDocumentList).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/datasets/my-dataset-123/documents')
})
it('should call invalidDocumentList before navigation', () => {
// Arrange
const props = createDefaultProps()
const callOrder: string[] = []
mockInvalidDocumentList.mockImplementation(() => callOrder.push('invalidate'))
mockPush.mockImplementation(() => callOrder.push('push'))
// Act
render(<EmbeddingProcess {...props} />)
const navButton = screen.getByText('datasetCreation.stepThree.navTo')
fireEvent.click(navButton)
// Assert
expect(callOrder).toEqual(['invalidate', 'push'])
})
})
// ==========================================
// Document Display Tests
// ==========================================
describe('Document Display', () => {
// Tests for document list rendering
it('should display document names', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1', name: 'my-report.pdf' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('my-report.pdf')).toBeInTheDocument()
})
it('should display multiple documents', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1', name: 'file1.txt' })
const doc2 = createMockDocument({ id: 'doc-2', name: 'file2.pdf' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
createMockIndexingStatus({ id: 'doc-2', indexing_status: 'waiting' }),
]
const props = createDefaultProps({ documents: [doc1, doc2] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('file1.txt')).toBeInTheDocument()
expect(screen.getByText('file2.pdf')).toBeInTheDocument()
})
it('should handle documents with special characters in names', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1', name: 'report_2024 (final) - copy.pdf' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('report_2024 (final) - copy.pdf')).toBeInTheDocument()
})
})
// ==========================================
// Data Source Type Tests
// ==========================================
describe('Data Source Types', () => {
// Tests for different data source type displays
it('should handle local file data source', async () => {
// Arrange
const doc1 = createMockDocument({
id: 'doc-1',
name: 'local-file.pdf',
data_source_type: DatasourceType.localFile,
})
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('local-file.pdf')).toBeInTheDocument()
})
it('should handle online document data source', async () => {
// Arrange
const doc1 = createMockDocument({
id: 'doc-1',
name: 'Notion Page',
data_source_type: DatasourceType.onlineDocument,
data_source_info: { notion_page_icon: { type: 'emoji', emoji: '📄' } },
})
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('Notion Page')).toBeInTheDocument()
})
it('should handle website crawl data source', async () => {
// Arrange
const doc1 = createMockDocument({
id: 'doc-1',
name: 'https://example.com/page',
data_source_type: DatasourceType.websiteCrawl,
})
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('https://example.com/page')).toBeInTheDocument()
})
it('should handle online drive data source', async () => {
// Arrange
const doc1 = createMockDocument({
id: 'doc-1',
name: 'Google Drive Document',
data_source_type: DatasourceType.onlineDrive,
})
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('Google Drive Document')).toBeInTheDocument()
})
})
// ==========================================
// Error Handling Tests
// ==========================================
describe('Error Handling', () => {
// Tests for error states and displays
it('should display error icon for documents with error status', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: 'error',
error: 'Failed to process document',
}),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
const { container } = render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert - error icon should be visible
const errorIcon = container.querySelector('.text-text-destructive')
expect(errorIcon).toBeInTheDocument()
})
it('should apply error styling to document row with error', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: 'error',
error: 'Processing failed',
}),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
const { container } = render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert - should have error background class
const errorRow = container.querySelector('.bg-state-destructive-hover-alt')
expect(errorRow).toBeInTheDocument()
})
})
// ==========================================
// Edge Cases
// ==========================================
describe('Edge Cases', () => {
// Tests for boundary conditions
it('should throw error when documents array is empty', () => {
// Arrange
// The component accesses documents[0].id for useProcessRule (line 81-82),
// which throws TypeError when documents array is empty.
// This test documents this known limitation.
const props = createDefaultProps({ documents: [] })
// Suppress console errors for expected error
const consoleError = jest.spyOn(console, 'error').mockImplementation(Function.prototype as () => void)
// Act & Assert - explicitly assert the error behavior
expect(() => {
render(<EmbeddingProcess {...props} />)
}).toThrow(TypeError)
consoleError.mockRestore()
})
it('should handle empty indexing status response', async () => {
// Arrange
mockIndexingStatusData = []
const props = createDefaultProps()
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert - should not show any status text when empty
expect(screen.queryByText('datasetDocuments.embedding.waiting')).not.toBeInTheDocument()
expect(screen.queryByText('datasetDocuments.embedding.processing')).not.toBeInTheDocument()
expect(screen.queryByText('datasetDocuments.embedding.completed')).not.toBeInTheDocument()
})
it('should handle document with undefined name', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1', name: undefined as unknown as string })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act & Assert - should not throw
expect(() => render(<EmbeddingProcess {...props} />)).not.toThrow()
})
it('should handle document not found in indexing status', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'other-doc', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act & Assert - should not throw
expect(() => render(<EmbeddingProcess {...props} />)).not.toThrow()
})
it('should handle undefined indexing_status', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({
id: 'doc-1',
indexing_status: undefined as unknown as DocumentIndexingStatus,
}),
]
const props = createDefaultProps({ documents: [doc1] })
// Act & Assert - should not throw
expect(() => render(<EmbeddingProcess {...props} />)).not.toThrow()
})
it('should handle mixed status documents', async () => {
// Arrange
const doc1 = createMockDocument({ id: 'doc-1' })
const doc2 = createMockDocument({ id: 'doc-2' })
const doc3 = createMockDocument({ id: 'doc-3' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'completed' }),
createMockIndexingStatus({ id: 'doc-2', indexing_status: 'indexing' }),
createMockIndexingStatus({ id: 'doc-3', indexing_status: 'error' }),
]
const props = createDefaultProps({ documents: [doc1, doc2, doc3] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert - should show processing (since one is still indexing)
expect(screen.getByText('datasetDocuments.embedding.processing')).toBeInTheDocument()
})
})
// ==========================================
// Props Variations Tests
// ==========================================
describe('Props Variations', () => {
// Tests for different prop combinations
it('should handle undefined indexingType', () => {
// Arrange
const props = createDefaultProps({ indexingType: undefined })
// Act
render(<EmbeddingProcess {...props} />)
// Assert - component renders without crashing
expect(screen.getByTestId('rule-detail')).toBeInTheDocument()
})
it('should handle undefined retrievalMethod', () => {
// Arrange
const props = createDefaultProps({ retrievalMethod: undefined })
// Act
render(<EmbeddingProcess {...props} />)
// Assert - component renders without crashing
expect(screen.getByTestId('rule-detail')).toBeInTheDocument()
})
it('should pass different indexingType values', () => {
// Arrange
const indexingTypes = [IndexingType.QUALIFIED, IndexingType.ECONOMICAL]
indexingTypes.forEach((indexingType) => {
const props = createDefaultProps({ indexingType })
// Act
const { unmount } = render(<EmbeddingProcess {...props} />)
// Assert - RuleDetail renders and shows appropriate text based on indexingType
expect(screen.getByTestId('rule-detail')).toBeInTheDocument()
unmount()
})
})
it('should pass different retrievalMethod values', () => {
// Arrange
const retrievalMethods = [RETRIEVE_METHOD.semantic, RETRIEVE_METHOD.fullText, RETRIEVE_METHOD.hybrid]
retrievalMethods.forEach((retrievalMethod) => {
const props = createDefaultProps({ retrievalMethod })
// Act
const { unmount } = render(<EmbeddingProcess {...props} />)
// Assert - RuleDetail renders and shows appropriate text based on retrievalMethod
expect(screen.getByTestId('rule-detail')).toBeInTheDocument()
unmount()
})
})
})
// ==========================================
// Memoization Tests
// ==========================================
describe('Memoization Logic', () => {
// Tests for useMemo computed values
it('should correctly compute isEmbeddingWaiting', async () => {
// Arrange - all waiting
const doc1 = createMockDocument({ id: 'doc-1' })
const doc2 = createMockDocument({ id: 'doc-2' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'waiting' }),
createMockIndexingStatus({ id: 'doc-2', indexing_status: 'waiting' }),
]
const props = createDefaultProps({ documents: [doc1, doc2] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.waiting')).toBeInTheDocument()
})
it('should correctly compute isEmbedding when one is indexing', async () => {
// Arrange - one waiting, one indexing
const doc1 = createMockDocument({ id: 'doc-1' })
const doc2 = createMockDocument({ id: 'doc-2' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'waiting' }),
createMockIndexingStatus({ id: 'doc-2', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1, doc2] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.processing')).toBeInTheDocument()
})
it('should correctly compute isEmbeddingCompleted for mixed terminal states', async () => {
// Arrange - completed + error + paused = all terminal
const doc1 = createMockDocument({ id: 'doc-1' })
const doc2 = createMockDocument({ id: 'doc-2' })
const doc3 = createMockDocument({ id: 'doc-3' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'completed' }),
createMockIndexingStatus({ id: 'doc-2', indexing_status: 'error' }),
createMockIndexingStatus({ id: 'doc-3', indexing_status: 'paused' }),
]
const props = createDefaultProps({ documents: [doc1, doc2, doc3] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('datasetDocuments.embedding.completed')).toBeInTheDocument()
})
})
// ==========================================
// File Type Detection Tests
// ==========================================
describe('File Type Detection', () => {
// Tests for getFileType helper function
it('should extract file extension correctly', async () => {
// Arrange
const doc1 = createMockDocument({
id: 'doc-1',
name: 'document.pdf',
data_source_type: DatasourceType.localFile,
})
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert - file should be displayed (file type detection happens internally)
expect(screen.getByText('document.pdf')).toBeInTheDocument()
})
it('should handle files with multiple dots', async () => {
// Arrange
const doc1 = createMockDocument({
id: 'doc-1',
name: 'my.report.2024.pdf',
data_source_type: DatasourceType.localFile,
})
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('my.report.2024.pdf')).toBeInTheDocument()
})
it('should handle files without extension', async () => {
// Arrange
const doc1 = createMockDocument({
id: 'doc-1',
name: 'README',
data_source_type: DatasourceType.localFile,
})
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert
expect(screen.getByText('README')).toBeInTheDocument()
})
})
// ==========================================
// Priority Label Tests
// ==========================================
describe('Priority Label', () => {
// Tests for priority label display
it('should show priority label when billing is enabled', async () => {
// Arrange
mockEnableBilling = true
mockPlanType = Plan.sandbox
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
const { container } = render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert - PriorityLabel component should be rendered
// Since we don't mock PriorityLabel, we check the structure exists
expect(container.querySelector('.ml-0')).toBeInTheDocument()
})
it('should not show priority label when billing is disabled', async () => {
// Arrange
mockEnableBilling = false
const doc1 = createMockDocument({ id: 'doc-1' })
mockIndexingStatusData = [
createMockIndexingStatus({ id: 'doc-1', indexing_status: 'indexing' }),
]
const props = createDefaultProps({ documents: [doc1] })
// Act
render(<EmbeddingProcess {...props} />)
await waitFor(() => {
expect(mockFetchIndexingStatus).toHaveBeenCalled()
})
// Assert - upgrade banner should not be present
expect(screen.queryByText('billing.plansCommon.documentProcessingPriorityUpgrade')).not.toBeInTheDocument()
})
})
})