test: add tests for dataset document detail (#31274)

Co-authored-by: CodingOnStar <hanxujiang@dify.ai>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
Coding On Star
2026-01-27 15:43:27 +08:00
committed by GitHub
parent eca26a9b9b
commit c8abe1c306
105 changed files with 28225 additions and 686 deletions

View File

@ -0,0 +1,407 @@
import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// ============================================================================
// Component Imports (after mocks)
// ============================================================================
import UrlInput from './url-input'
// ============================================================================
// Mock Setup
// ============================================================================
// Mock useDocLink hook
vi.mock('@/context/i18n', () => ({
useDocLink: vi.fn(() => () => 'https://docs.example.com'),
}))
// ============================================================================
// UrlInput Component Tests
// ============================================================================
describe('UrlInput', () => {
const mockOnRun = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should render input with placeholder from docLink', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
expect(input).toHaveAttribute('placeholder', 'https://docs.example.com')
})
it('should render button with run text when not running', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
expect(button).toHaveTextContent(/run/i)
})
it('should render button without run text when running', () => {
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const button = screen.getByRole('button')
// Button should not have "run" text when running (shows loading state instead)
expect(button).not.toHaveTextContent(/run/i)
})
it('should show loading state on button when running', () => {
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
// Button should show loading text when running
const button = screen.getByRole('button')
expect(button).toHaveTextContent(/loading/i)
})
it('should not show loading state on button when not running', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
expect(button).not.toHaveTextContent(/loading/i)
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should update input value when user types', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://example.com')
expect(input).toHaveValue('https://example.com')
})
it('should call onRun with url when button is clicked and not running', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://example.com')
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith('https://example.com')
expect(mockOnRun).toHaveBeenCalledTimes(1)
})
it('should NOT call onRun when button is clicked and isRunning is true', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
// Use fireEvent since userEvent might not work well with disabled-like states
fireEvent.change(input, { target: { value: 'https://example.com' } })
const button = screen.getByRole('button')
await user.click(button)
// onRun should NOT be called because isRunning is true
expect(mockOnRun).not.toHaveBeenCalled()
})
it('should call onRun with empty string when button clicked with empty input', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith('')
expect(mockOnRun).toHaveBeenCalledTimes(1)
})
it('should handle multiple button clicks when not running', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://test.com')
const button = screen.getByRole('button')
await user.click(button)
await user.click(button)
expect(mockOnRun).toHaveBeenCalledTimes(2)
expect(mockOnRun).toHaveBeenCalledWith('https://test.com')
})
})
// --------------------------------------------------------------------------
// Props Variations Tests
// --------------------------------------------------------------------------
describe('Props Variations', () => {
it('should update button state when isRunning changes from false to true', () => {
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
expect(button).toHaveTextContent(/run/i)
rerender(<UrlInput isRunning={true} onRun={mockOnRun} />)
// When running, button shows loading state instead of "run" text
expect(button).not.toHaveTextContent(/run/i)
})
it('should update button state when isRunning changes from true to false', () => {
const { rerender } = render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const button = screen.getByRole('button')
// When running, button shows loading state instead of "run" text
expect(button).not.toHaveTextContent(/run/i)
rerender(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(button).toHaveTextContent(/run/i)
})
it('should preserve input value when isRunning prop changes', async () => {
const user = userEvent.setup()
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'https://preserved.com')
expect(input).toHaveValue('https://preserved.com')
rerender(<UrlInput isRunning={true} onRun={mockOnRun} />)
expect(input).toHaveValue('https://preserved.com')
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle special characters in url', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
const specialUrl = 'https://example.com/path?query=test&param=value#anchor'
await user.type(input, specialUrl)
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith(specialUrl)
})
it('should handle unicode characters in url', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
const unicodeUrl = 'https://example.com/路径/文件'
await user.type(input, unicodeUrl)
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith(unicodeUrl)
})
it('should handle very long url', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
const longUrl = `https://example.com/${'a'.repeat(500)}`
// Use fireEvent for long text to avoid timeout
fireEvent.change(input, { target: { value: longUrl } })
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledWith(longUrl)
})
it('should handle whitespace in url', async () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: ' https://example.com ' } })
const button = screen.getByRole('button')
fireEvent.click(button)
expect(mockOnRun).toHaveBeenCalledWith(' https://example.com ')
})
it('should handle rapid input changes', async () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'a' } })
fireEvent.change(input, { target: { value: 'ab' } })
fireEvent.change(input, { target: { value: 'abc' } })
fireEvent.change(input, { target: { value: 'https://final.com' } })
expect(input).toHaveValue('https://final.com')
const button = screen.getByRole('button')
fireEvent.click(button)
expect(mockOnRun).toHaveBeenCalledWith('https://final.com')
})
})
// --------------------------------------------------------------------------
// handleOnRun Branch Coverage Tests
// --------------------------------------------------------------------------
describe('handleOnRun Branch Coverage', () => {
it('should return early when isRunning is true (branch: isRunning = true)', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'https://test.com' } })
const button = screen.getByRole('button')
await user.click(button)
// The early return should prevent onRun from being called
expect(mockOnRun).not.toHaveBeenCalled()
})
it('should call onRun when isRunning is false (branch: isRunning = false)', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
fireEvent.change(input, { target: { value: 'https://test.com' } })
const button = screen.getByRole('button')
await user.click(button)
// onRun should be called when isRunning is false
expect(mockOnRun).toHaveBeenCalledWith('https://test.com')
})
})
// --------------------------------------------------------------------------
// Button Text Branch Coverage Tests
// --------------------------------------------------------------------------
describe('Button Text Branch Coverage', () => {
it('should display run text when isRunning is false (branch: !isRunning = true)', () => {
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
// When !isRunning is true, button shows the translated "run" text
expect(button).toHaveTextContent(/run/i)
})
it('should not display run text when isRunning is true (branch: !isRunning = false)', () => {
render(<UrlInput isRunning={true} onRun={mockOnRun} />)
const button = screen.getByRole('button')
// When !isRunning is false, button shows empty string '' (loading state shows spinner)
expect(button).not.toHaveTextContent(/run/i)
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
rerender(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should use useCallback for handleUrlChange', async () => {
const user = userEvent.setup()
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
const input = screen.getByRole('textbox')
await user.type(input, 'test')
rerender(<UrlInput isRunning={false} onRun={mockOnRun} />)
// Input should maintain value after rerender
expect(input).toHaveValue('test')
})
it('should use useCallback for handleOnRun', async () => {
const user = userEvent.setup()
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
rerender(<UrlInput isRunning={false} onRun={mockOnRun} />)
const button = screen.getByRole('button')
await user.click(button)
expect(mockOnRun).toHaveBeenCalledTimes(1)
})
})
// --------------------------------------------------------------------------
// Integration Tests
// --------------------------------------------------------------------------
describe('Integration', () => {
it('should complete full workflow: type url -> click run -> verify callback', async () => {
const user = userEvent.setup()
render(<UrlInput isRunning={false} onRun={mockOnRun} />)
// Type URL
const input = screen.getByRole('textbox')
await user.type(input, 'https://mywebsite.com')
// Click run
const button = screen.getByRole('button')
await user.click(button)
// Verify callback
expect(mockOnRun).toHaveBeenCalledWith('https://mywebsite.com')
})
it('should show correct states during running workflow', () => {
const { rerender } = render(<UrlInput isRunning={false} onRun={mockOnRun} />)
// Initial state: not running
expect(screen.getByRole('button')).toHaveTextContent(/run/i)
// Simulate running state
rerender(<UrlInput isRunning={true} onRun={mockOnRun} />)
expect(screen.getByRole('button')).not.toHaveTextContent(/run/i)
// Simulate finished state
rerender(<UrlInput isRunning={false} onRun={mockOnRun} />)
expect(screen.getByRole('button')).toHaveTextContent(/run/i)
})
})
})

View File

@ -0,0 +1,701 @@
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// ============================================================================
// Component Import (after mocks)
// ============================================================================
import FireCrawl from './index'
// ============================================================================
// Mock Setup - Only mock API calls and context
// ============================================================================
// Mock API service
const mockCreateFirecrawlTask = vi.fn()
const mockCheckFirecrawlTaskStatus = vi.fn()
vi.mock('@/service/datasets', () => ({
createFirecrawlTask: (...args: unknown[]) => mockCreateFirecrawlTask(...args),
checkFirecrawlTaskStatus: (...args: unknown[]) => mockCheckFirecrawlTaskStatus(...args),
}))
// Mock modal context
const mockSetShowAccountSettingModal = vi.fn()
vi.mock('@/context/modal-context', () => ({
useModalContextSelector: vi.fn(() => mockSetShowAccountSettingModal),
}))
// Mock sleep utility to speed up tests
vi.mock('@/utils', () => ({
sleep: vi.fn(() => Promise.resolve()),
}))
// Mock useDocLink hook for UrlInput placeholder
vi.mock('@/context/i18n', () => ({
useDocLink: vi.fn(() => () => 'https://docs.example.com'),
}))
// ============================================================================
// Test Data Factory
// ============================================================================
const createMockCrawlOptions = (overrides: Partial<CrawlOptions> = {}): CrawlOptions => ({
crawl_sub_pages: true,
limit: 10,
max_depth: 2,
excludes: '',
includes: '',
only_main_content: false,
use_sitemap: false,
...overrides,
})
const createMockCrawlResultItem = (overrides: Partial<CrawlResultItem> = {}): CrawlResultItem => ({
title: 'Test Page',
markdown: '# Test Content',
description: 'Test page description',
source_url: 'https://example.com/page',
...overrides,
})
// ============================================================================
// FireCrawl Component Tests
// ============================================================================
describe('FireCrawl', () => {
const mockOnPreview = vi.fn()
const mockOnCheckedCrawlResultChange = vi.fn()
const mockOnJobIdChange = vi.fn()
const mockOnCrawlOptionsChange = vi.fn()
const defaultProps = {
onPreview: mockOnPreview,
checkedCrawlResult: [] as CrawlResultItem[],
onCheckedCrawlResultChange: mockOnCheckedCrawlResultChange,
onJobIdChange: mockOnJobIdChange,
crawlOptions: createMockCrawlOptions(),
onCrawlOptionsChange: mockOnCrawlOptionsChange,
}
beforeEach(() => {
vi.clearAllMocks()
mockCreateFirecrawlTask.mockReset()
mockCheckFirecrawlTaskStatus.mockReset()
})
// Helper to get URL input (first textbox with specific placeholder)
const getUrlInput = () => {
return screen.getByPlaceholderText('https://docs.example.com')
}
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
render(<FireCrawl {...defaultProps} />)
expect(screen.getByText(/firecrawlTitle/i)).toBeInTheDocument()
})
it('should render Header component with correct props', () => {
render(<FireCrawl {...defaultProps} />)
expect(screen.getByText(/firecrawlTitle/i)).toBeInTheDocument()
expect(screen.getByText(/configureFirecrawl/i)).toBeInTheDocument()
expect(screen.getByText(/firecrawlDoc/i)).toBeInTheDocument()
})
it('should render UrlInput component', () => {
render(<FireCrawl {...defaultProps} />)
expect(getUrlInput()).toBeInTheDocument()
expect(screen.getByRole('button', { name: /run/i })).toBeInTheDocument()
})
it('should render Options component', () => {
render(<FireCrawl {...defaultProps} />)
expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument()
expect(screen.getByText(/limit/i)).toBeInTheDocument()
})
it('should not render crawling or result components initially', () => {
render(<FireCrawl {...defaultProps} />)
// Crawling and result components should not be visible in init state
expect(screen.queryByText(/crawling/i)).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Configuration Button Tests
// --------------------------------------------------------------------------
describe('Configuration Button', () => {
it('should call setShowAccountSettingModal when configure button is clicked', async () => {
const user = userEvent.setup()
render(<FireCrawl {...defaultProps} />)
const configButton = screen.getByText(/configureFirecrawl/i)
await user.click(configButton)
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
payload: 'data-source',
})
})
})
// --------------------------------------------------------------------------
// URL Validation Tests
// --------------------------------------------------------------------------
describe('URL Validation', () => {
it('should show error toast when URL is empty', async () => {
const user = userEvent.setup()
render(<FireCrawl {...defaultProps} />)
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
// Should not call API when validation fails
expect(mockCreateFirecrawlTask).not.toHaveBeenCalled()
})
it('should show error toast when URL does not start with http:// or https://', async () => {
const user = userEvent.setup()
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'invalid-url.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
// Should not call API when validation fails
expect(mockCreateFirecrawlTask).not.toHaveBeenCalled()
})
it('should show error toast when limit is empty', async () => {
const user = userEvent.setup()
const propsWithEmptyLimit = {
...defaultProps,
crawlOptions: createMockCrawlOptions({ limit: '' as unknown as number }),
}
render(<FireCrawl {...propsWithEmptyLimit} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
// Should not call API when validation fails
expect(mockCreateFirecrawlTask).not.toHaveBeenCalled()
})
it('should show error toast when limit is null', async () => {
const user = userEvent.setup()
const propsWithNullLimit = {
...defaultProps,
crawlOptions: createMockCrawlOptions({ limit: null as unknown as number }),
}
render(<FireCrawl {...propsWithNullLimit} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
expect(mockCreateFirecrawlTask).not.toHaveBeenCalled()
})
it('should accept valid http:// URL', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job-id' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: [],
total: 0,
current: 0,
time_consuming: 1,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'http://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockCreateFirecrawlTask).toHaveBeenCalled()
})
})
it('should accept valid https:// URL', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job-id' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: [],
total: 0,
current: 0,
time_consuming: 1,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockCreateFirecrawlTask).toHaveBeenCalled()
})
})
})
// --------------------------------------------------------------------------
// Crawl Execution Tests
// --------------------------------------------------------------------------
describe('Crawl Execution', () => {
it('should call createFirecrawlTask with correct parameters', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job-id' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: [],
total: 0,
current: 0,
time_consuming: 1,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockCreateFirecrawlTask).toHaveBeenCalledWith({
url: 'https://example.com',
options: expect.objectContaining({
crawl_sub_pages: true,
limit: 10,
max_depth: 2,
}),
})
})
})
it('should call onJobIdChange with job_id from API response', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'my-job-123' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: [],
total: 0,
current: 0,
time_consuming: 1,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockOnJobIdChange).toHaveBeenCalledWith('my-job-123')
})
})
it('should remove empty max_depth from crawlOptions before sending to API', async () => {
const user = userEvent.setup()
const propsWithEmptyMaxDepth = {
...defaultProps,
crawlOptions: createMockCrawlOptions({ max_depth: '' as unknown as number }),
}
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job-id' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: [],
total: 0,
current: 0,
time_consuming: 1,
})
render(<FireCrawl {...propsWithEmptyMaxDepth} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockCreateFirecrawlTask).toHaveBeenCalledWith({
url: 'https://example.com',
options: expect.not.objectContaining({
max_depth: '',
}),
})
})
})
it('should show loading state while running', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockImplementation(() => new Promise(() => {})) // Never resolves
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
// Button should show loading state (no longer show "run" text)
await waitFor(() => {
expect(runButton).not.toHaveTextContent(/run/i)
})
})
})
// --------------------------------------------------------------------------
// Crawl Status Polling Tests
// --------------------------------------------------------------------------
describe('Crawl Status Polling', () => {
it('should handle completed status', async () => {
const user = userEvent.setup()
const mockResults = [createMockCrawlResultItem()]
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: mockResults,
total: 1,
current: 1,
time_consuming: 2.5,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalledWith(mockResults)
})
})
it('should handle error status from API', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'error',
message: 'Crawl failed',
data: [],
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(screen.getByText(/exceptionErrorTitle/i)).toBeInTheDocument()
})
})
it('should handle missing status as error', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: undefined,
message: 'No status',
data: [],
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(screen.getByText(/exceptionErrorTitle/i)).toBeInTheDocument()
})
})
it('should poll again when status is pending', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus
.mockResolvedValueOnce({
status: 'pending',
data: [{ title: 'Page 1', markdown: 'content', source_url: 'https://example.com/1' }],
total: 5,
current: 1,
})
.mockResolvedValueOnce({
status: 'completed',
data: [{ title: 'Page 1', markdown: 'content', source_url: 'https://example.com/1' }],
total: 5,
current: 5,
time_consuming: 3,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockCheckFirecrawlTaskStatus).toHaveBeenCalledTimes(2)
})
})
it('should update progress during crawling', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus
.mockResolvedValueOnce({
status: 'pending',
data: [{ title: 'Page 1', markdown: 'content', source_url: 'https://example.com/1' }],
total: 10,
current: 3,
})
.mockResolvedValueOnce({
status: 'completed',
data: [{ title: 'Page 1', markdown: 'content', source_url: 'https://example.com/1' }],
total: 10,
current: 10,
time_consuming: 5,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(mockOnCheckedCrawlResultChange).toHaveBeenCalled()
})
})
})
// --------------------------------------------------------------------------
// Error Handling Tests
// --------------------------------------------------------------------------
describe('Error Handling', () => {
it('should handle API exception during task creation', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockRejectedValueOnce(new Error('Network error'))
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(screen.getByText(/exceptionErrorTitle/i)).toBeInTheDocument()
})
})
it('should handle API exception during status check', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockRejectedValueOnce({
json: () => Promise.resolve({ message: 'Status check failed' }),
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(screen.getByText(/exceptionErrorTitle/i)).toBeInTheDocument()
})
})
it('should display error message from API', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'error',
message: 'Custom error message',
data: [],
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(screen.getByText('Custom error message')).toBeInTheDocument()
})
})
it('should display unknown error when no error message provided', async () => {
const user = userEvent.setup()
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'error',
message: undefined,
data: [],
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(screen.getByText(/unknownError/i)).toBeInTheDocument()
})
})
})
// --------------------------------------------------------------------------
// Options Change Tests
// --------------------------------------------------------------------------
describe('Options Change', () => {
it('should call onCrawlOptionsChange when options change', () => {
render(<FireCrawl {...defaultProps} />)
// Find and change limit input
const limitInput = screen.getByDisplayValue('10')
fireEvent.change(limitInput, { target: { value: '20' } })
expect(mockOnCrawlOptionsChange).toHaveBeenCalledWith(
expect.objectContaining({ limit: 20 }),
)
})
it('should call onCrawlOptionsChange when checkbox changes', () => {
const { container } = render(<FireCrawl {...defaultProps} />)
// Use data-testid to find checkboxes since they are custom div elements
const checkboxes = container.querySelectorAll('[data-testid^="checkbox-"]')
fireEvent.click(checkboxes[0]) // crawl_sub_pages
expect(mockOnCrawlOptionsChange).toHaveBeenCalledWith(
expect.objectContaining({ crawl_sub_pages: false }),
)
})
})
// --------------------------------------------------------------------------
// Crawled Result Display Tests
// --------------------------------------------------------------------------
describe('Crawled Result Display', () => {
it('should display CrawledResult when crawl is finished successfully', async () => {
const user = userEvent.setup()
const mockResults = [
createMockCrawlResultItem({ title: 'Result Page 1' }),
createMockCrawlResultItem({ title: 'Result Page 2' }),
]
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: mockResults,
total: 2,
current: 2,
time_consuming: 1.5,
})
render(<FireCrawl {...defaultProps} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
expect(screen.getByText('Result Page 1')).toBeInTheDocument()
expect(screen.getByText('Result Page 2')).toBeInTheDocument()
})
})
it('should limit total to crawlOptions.limit', async () => {
const user = userEvent.setup()
const propsWithLimit5 = {
...defaultProps,
crawlOptions: createMockCrawlOptions({ limit: 5 }),
}
mockCreateFirecrawlTask.mockResolvedValueOnce({ job_id: 'test-job' })
mockCheckFirecrawlTaskStatus.mockResolvedValueOnce({
status: 'completed',
data: [],
total: 100, // API returns more than limit
current: 5,
time_consuming: 1,
})
render(<FireCrawl {...propsWithLimit5} />)
const input = getUrlInput()
await user.type(input, 'https://example.com')
const runButton = screen.getByRole('button', { name: /run/i })
await user.click(runButton)
await waitFor(() => {
// Total should be capped to limit (5)
expect(mockCheckFirecrawlTaskStatus).toHaveBeenCalled()
})
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const { rerender } = render(<FireCrawl {...defaultProps} />)
rerender(<FireCrawl {...defaultProps} />)
expect(screen.getByText(/firecrawlTitle/i)).toBeInTheDocument()
})
})
})

View File

@ -0,0 +1,405 @@
import type { CrawlOptions } from '@/models/datasets'
import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import Options from './options'
// ============================================================================
// Test Data Factory
// ============================================================================
const createMockCrawlOptions = (overrides: Partial<CrawlOptions> = {}): CrawlOptions => ({
crawl_sub_pages: true,
limit: 10,
max_depth: 2,
excludes: '',
includes: '',
only_main_content: false,
use_sitemap: false,
...overrides,
})
// ============================================================================
// Options Component Tests
// ============================================================================
describe('Options', () => {
const mockOnChange = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
})
// Helper to get checkboxes by test id pattern
const getCheckboxes = (container: HTMLElement) => {
return container.querySelectorAll('[data-testid^="checkbox-"]')
}
// --------------------------------------------------------------------------
// Rendering Tests
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
// Check that key elements are rendered
expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument()
expect(screen.getByText(/limit/i)).toBeInTheDocument()
expect(screen.getByText(/maxDepth/i)).toBeInTheDocument()
})
it('should render all form fields', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
// Checkboxes
expect(screen.getByText(/crawlSubPage/i)).toBeInTheDocument()
expect(screen.getByText(/extractOnlyMainContent/i)).toBeInTheDocument()
// Text/Number fields
expect(screen.getByText(/limit/i)).toBeInTheDocument()
expect(screen.getByText(/maxDepth/i)).toBeInTheDocument()
expect(screen.getByText(/excludePaths/i)).toBeInTheDocument()
expect(screen.getByText(/includeOnlyPaths/i)).toBeInTheDocument()
})
it('should render with custom className', () => {
const payload = createMockCrawlOptions()
const { container } = render(
<Options payload={payload} onChange={mockOnChange} className="custom-class" />,
)
const rootElement = container.firstChild as HTMLElement
expect(rootElement).toHaveClass('custom-class')
})
it('should render limit field with required indicator', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
// Limit field should have required indicator (*)
const requiredIndicator = screen.getByText('*')
expect(requiredIndicator).toBeInTheDocument()
})
it('should render placeholder for excludes field', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
const excludesInput = screen.getByPlaceholderText('blog/*, /about/*')
expect(excludesInput).toBeInTheDocument()
})
it('should render placeholder for includes field', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
const includesInput = screen.getByPlaceholderText('articles/*')
expect(includesInput).toBeInTheDocument()
})
it('should render two checkboxes', () => {
const payload = createMockCrawlOptions()
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
expect(checkboxes.length).toBe(2)
})
})
// --------------------------------------------------------------------------
// Props Display Tests
// --------------------------------------------------------------------------
describe('Props Display', () => {
it('should display crawl_sub_pages checkbox with check icon when true', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
// First checkbox should have check icon when checked
expect(checkboxes[0].querySelector('svg')).toBeInTheDocument()
})
it('should display crawl_sub_pages checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
// First checkbox should not have check icon when unchecked
expect(checkboxes[0].querySelector('svg')).not.toBeInTheDocument()
})
it('should display only_main_content checkbox with check icon when true', () => {
const payload = createMockCrawlOptions({ only_main_content: true })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
// Second checkbox should have check icon when checked
expect(checkboxes[1].querySelector('svg')).toBeInTheDocument()
})
it('should display only_main_content checkbox without check icon when false', () => {
const payload = createMockCrawlOptions({ only_main_content: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
// Second checkbox should not have check icon when unchecked
expect(checkboxes[1].querySelector('svg')).not.toBeInTheDocument()
})
it('should display limit value in input', () => {
const payload = createMockCrawlOptions({ limit: 25 })
render(<Options payload={payload} onChange={mockOnChange} />)
const limitInput = screen.getByDisplayValue('25')
expect(limitInput).toBeInTheDocument()
})
it('should display max_depth value in input', () => {
const payload = createMockCrawlOptions({ max_depth: 5 })
render(<Options payload={payload} onChange={mockOnChange} />)
const maxDepthInput = screen.getByDisplayValue('5')
expect(maxDepthInput).toBeInTheDocument()
})
it('should display excludes value in input', () => {
const payload = createMockCrawlOptions({ excludes: 'test/*' })
render(<Options payload={payload} onChange={mockOnChange} />)
const excludesInput = screen.getByDisplayValue('test/*')
expect(excludesInput).toBeInTheDocument()
})
it('should display includes value in input', () => {
const payload = createMockCrawlOptions({ includes: 'docs/*' })
render(<Options payload={payload} onChange={mockOnChange} />)
const includesInput = screen.getByDisplayValue('docs/*')
expect(includesInput).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call onChange with updated crawl_sub_pages when checkbox is clicked', () => {
const payload = createMockCrawlOptions({ crawl_sub_pages: true })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[0])
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
crawl_sub_pages: false,
})
})
it('should call onChange with updated only_main_content when checkbox is clicked', () => {
const payload = createMockCrawlOptions({ only_main_content: false })
const { container } = render(<Options payload={payload} onChange={mockOnChange} />)
const checkboxes = getCheckboxes(container)
fireEvent.click(checkboxes[1])
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
only_main_content: true,
})
})
it('should call onChange with updated limit when input changes', () => {
const payload = createMockCrawlOptions({ limit: 10 })
render(<Options payload={payload} onChange={mockOnChange} />)
const limitInput = screen.getByDisplayValue('10')
fireEvent.change(limitInput, { target: { value: '50' } })
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
limit: 50,
})
})
it('should call onChange with updated max_depth when input changes', () => {
const payload = createMockCrawlOptions({ max_depth: 2 })
render(<Options payload={payload} onChange={mockOnChange} />)
const maxDepthInput = screen.getByDisplayValue('2')
fireEvent.change(maxDepthInput, { target: { value: '10' } })
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
max_depth: 10,
})
})
it('should call onChange with updated excludes when input changes', () => {
const payload = createMockCrawlOptions({ excludes: '' })
render(<Options payload={payload} onChange={mockOnChange} />)
const excludesInput = screen.getByPlaceholderText('blog/*, /about/*')
fireEvent.change(excludesInput, { target: { value: 'admin/*' } })
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
excludes: 'admin/*',
})
})
it('should call onChange with updated includes when input changes', () => {
const payload = createMockCrawlOptions({ includes: '' })
render(<Options payload={payload} onChange={mockOnChange} />)
const includesInput = screen.getByPlaceholderText('articles/*')
fireEvent.change(includesInput, { target: { value: 'public/*' } })
expect(mockOnChange).toHaveBeenCalledWith({
...payload,
includes: 'public/*',
})
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle empty string values', () => {
const payload = createMockCrawlOptions({
limit: '',
max_depth: '',
excludes: '',
includes: '',
} as unknown as CrawlOptions)
render(<Options payload={payload} onChange={mockOnChange} />)
// Component should render without crashing
expect(screen.getByText(/limit/i)).toBeInTheDocument()
})
it('should handle zero values', () => {
const payload = createMockCrawlOptions({
limit: 0,
max_depth: 0,
})
render(<Options payload={payload} onChange={mockOnChange} />)
// Zero values should be displayed
const zeroInputs = screen.getAllByDisplayValue('0')
expect(zeroInputs.length).toBeGreaterThanOrEqual(1)
})
it('should handle large numbers', () => {
const payload = createMockCrawlOptions({
limit: 9999,
max_depth: 100,
})
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByDisplayValue('9999')).toBeInTheDocument()
expect(screen.getByDisplayValue('100')).toBeInTheDocument()
})
it('should handle special characters in text fields', () => {
const payload = createMockCrawlOptions({
excludes: 'path/*/file?query=1&param=2',
includes: 'docs/**/*.md',
})
render(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByDisplayValue('path/*/file?query=1&param=2')).toBeInTheDocument()
expect(screen.getByDisplayValue('docs/**/*.md')).toBeInTheDocument()
})
it('should preserve other payload fields when updating one field', () => {
const payload = createMockCrawlOptions({
crawl_sub_pages: true,
limit: 10,
max_depth: 2,
excludes: 'test/*',
includes: 'docs/*',
only_main_content: true,
})
render(<Options payload={payload} onChange={mockOnChange} />)
const limitInput = screen.getByDisplayValue('10')
fireEvent.change(limitInput, { target: { value: '20' } })
expect(mockOnChange).toHaveBeenCalledWith({
crawl_sub_pages: true,
limit: 20,
max_depth: 2,
excludes: 'test/*',
includes: 'docs/*',
only_main_content: true,
use_sitemap: false,
})
})
})
// --------------------------------------------------------------------------
// handleChange Callback Tests
// --------------------------------------------------------------------------
describe('handleChange Callback', () => {
it('should create a new callback for each key', () => {
const payload = createMockCrawlOptions()
render(<Options payload={payload} onChange={mockOnChange} />)
// Change limit
const limitInput = screen.getByDisplayValue('10')
fireEvent.change(limitInput, { target: { value: '15' } })
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({ limit: 15 }),
)
// Change max_depth
const maxDepthInput = screen.getByDisplayValue('2')
fireEvent.change(maxDepthInput, { target: { value: '5' } })
expect(mockOnChange).toHaveBeenCalledWith(
expect.objectContaining({ max_depth: 5 }),
)
})
it('should handle multiple rapid changes', () => {
const payload = createMockCrawlOptions({ limit: 10 })
render(<Options payload={payload} onChange={mockOnChange} />)
const limitInput = screen.getByDisplayValue('10')
fireEvent.change(limitInput, { target: { value: '11' } })
fireEvent.change(limitInput, { target: { value: '12' } })
fireEvent.change(limitInput, { target: { value: '13' } })
expect(mockOnChange).toHaveBeenCalledTimes(3)
})
})
// --------------------------------------------------------------------------
// Memoization Tests
// --------------------------------------------------------------------------
describe('Memoization', () => {
it('should be memoized with React.memo', () => {
const payload = createMockCrawlOptions()
const { rerender } = render(<Options payload={payload} onChange={mockOnChange} />)
rerender(<Options payload={payload} onChange={mockOnChange} />)
expect(screen.getByText(/limit/i)).toBeInTheDocument()
})
it('should re-render when payload changes', () => {
const payload1 = createMockCrawlOptions({ limit: 10 })
const payload2 = createMockCrawlOptions({ limit: 20 })
const { rerender } = render(<Options payload={payload1} onChange={mockOnChange} />)
expect(screen.getByDisplayValue('10')).toBeInTheDocument()
rerender(<Options payload={payload2} onChange={mockOnChange} />)
expect(screen.getByDisplayValue('20')).toBeInTheDocument()
})
})
})

View File

@ -70,6 +70,11 @@ const createDefaultProps = (overrides: Partial<Parameters<typeof JinaReader>[0]>
describe('JinaReader', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.useFakeTimers({ shouldAdvanceTime: true })
})
afterEach(() => {
vi.useRealTimers()
})
describe('Rendering', () => {
@ -158,7 +163,7 @@ describe('JinaReader', () => {
describe('Props', () => {
it('should call onCrawlOptionsChange when options change', async () => {
// Arrange
const user = userEvent.setup()
const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime })
const onCrawlOptionsChange = vi.fn()
const props = createDefaultProps({ onCrawlOptionsChange })
@ -237,9 +242,10 @@ describe('JinaReader', () => {
// Arrange
const mockCreateTask = createJinaReaderTask as Mock
let resolvePromise: () => void
mockCreateTask.mockImplementation(() => new Promise((resolve) => {
const taskPromise = new Promise((resolve) => {
resolvePromise = () => resolve({ data: { title: 'T', content: 'C', description: 'D', url: 'https://example.com' } })
}))
})
mockCreateTask.mockImplementation(() => taskPromise)
const props = createDefaultProps()
@ -257,8 +263,11 @@ describe('JinaReader', () => {
expect(screen.getByText(/totalPageScraped/i)).toBeInTheDocument()
})
// Cleanup - resolve the promise
// Cleanup - resolve the promise and wait for component to finish
resolvePromise!()
await waitFor(() => {
expect(screen.queryByText(/totalPageScraped/i)).not.toBeInTheDocument()
})
})
it('should transition to finished state after successful crawl', async () => {
@ -394,7 +403,11 @@ describe('JinaReader', () => {
it('should update controlFoldOptions when step changes', async () => {
// Arrange
const mockCreateTask = createJinaReaderTask as Mock
mockCreateTask.mockImplementation(() => new Promise((_resolve) => { /* pending */ }))
let resolvePromise: () => void
const taskPromise = new Promise((resolve) => {
resolvePromise = () => resolve({ data: { title: 'T', content: 'C', description: 'D', url: 'https://example.com' } })
})
mockCreateTask.mockImplementation(() => taskPromise)
const props = createDefaultProps()
@ -412,6 +425,12 @@ describe('JinaReader', () => {
await waitFor(() => {
expect(screen.getByText(/totalPageScraped/i)).toBeInTheDocument()
})
// Cleanup - resolve the promise
resolvePromise!()
await waitFor(() => {
expect(screen.queryByText(/totalPageScraped/i)).not.toBeInTheDocument()
})
})
})
@ -1073,9 +1092,13 @@ describe('JinaReader', () => {
// Arrange
const mockCreateTask = createJinaReaderTask as Mock
const mockCheckStatus = checkJinaReaderTaskStatus as Mock
let resolveCheckStatus: () => void
const checkStatusPromise = new Promise((resolve) => {
resolveCheckStatus = () => resolve({ status: 'completed', current: 0, total: 0, data: [] })
})
mockCreateTask.mockResolvedValueOnce({ job_id: 'zero-current-job' })
mockCheckStatus.mockImplementation(() => new Promise(() => { /* never resolves */ }))
mockCheckStatus.mockImplementation(() => checkStatusPromise)
const props = createDefaultProps({
crawlOptions: createDefaultCrawlOptions({ limit: 10 }),
@ -1091,15 +1114,25 @@ describe('JinaReader', () => {
await waitFor(() => {
expect(screen.getByText(/totalPageScraped.*0\/10/)).toBeInTheDocument()
})
// Cleanup - resolve the promise
resolveCheckStatus!()
await waitFor(() => {
expect(screen.queryByText(/totalPageScraped/i)).not.toBeInTheDocument()
})
})
it('should show 0/0 progress when limit is zero string', async () => {
// Arrange
const mockCreateTask = createJinaReaderTask as Mock
const mockCheckStatus = checkJinaReaderTaskStatus as Mock
let resolveCheckStatus: () => void
const checkStatusPromise = new Promise((resolve) => {
resolveCheckStatus = () => resolve({ status: 'completed', current: 0, total: 0, data: [] })
})
mockCreateTask.mockResolvedValueOnce({ job_id: 'zero-total-job' })
mockCheckStatus.mockImplementation(() => new Promise(() => { /* never resolves */ }))
mockCheckStatus.mockImplementation(() => checkStatusPromise)
const props = createDefaultProps({
crawlOptions: createDefaultCrawlOptions({ limit: '0' }),
@ -1115,6 +1148,12 @@ describe('JinaReader', () => {
await waitFor(() => {
expect(screen.getByText(/totalPageScraped.*0\/0/)).toBeInTheDocument()
})
// Cleanup - resolve the promise
resolveCheckStatus!()
await waitFor(() => {
expect(screen.queryByText(/totalPageScraped/i)).not.toBeInTheDocument()
})
})
it('should complete successfully when result data is undefined', async () => {
@ -1150,9 +1189,13 @@ describe('JinaReader', () => {
// Arrange
const mockCreateTask = createJinaReaderTask as Mock
const mockCheckStatus = checkJinaReaderTaskStatus as Mock
let resolveCheckStatus: () => void
const checkStatusPromise = new Promise((resolve) => {
resolveCheckStatus = () => resolve({ status: 'completed', current: 0, total: 0, data: [] })
})
mockCreateTask.mockResolvedValueOnce({ job_id: 'no-total-job' })
mockCheckStatus.mockImplementation(() => new Promise(() => { /* never resolves */ }))
mockCheckStatus.mockImplementation(() => checkStatusPromise)
const props = createDefaultProps({
crawlOptions: createDefaultCrawlOptions({ limit: 15 }),
@ -1168,12 +1211,22 @@ describe('JinaReader', () => {
await waitFor(() => {
expect(screen.getByText(/totalPageScraped.*0\/15/)).toBeInTheDocument()
})
// Cleanup - resolve the promise
resolveCheckStatus!()
await waitFor(() => {
expect(screen.queryByText(/totalPageScraped/i)).not.toBeInTheDocument()
})
})
it('should fallback to limit when crawlResult has zero total', async () => {
// Arrange
const mockCreateTask = createJinaReaderTask as Mock
const mockCheckStatus = checkJinaReaderTaskStatus as Mock
let resolveCheckStatus: () => void
const checkStatusPromise = new Promise((resolve) => {
resolveCheckStatus = () => resolve({ status: 'completed', current: 0, total: 0, data: [] })
})
mockCreateTask.mockResolvedValueOnce({ job_id: 'both-zero-job' })
mockCheckStatus
@ -1183,7 +1236,7 @@ describe('JinaReader', () => {
total: 0,
data: [],
})
.mockImplementationOnce(() => new Promise(() => { /* never resolves */ }))
.mockImplementationOnce(() => checkStatusPromise)
const props = createDefaultProps({
crawlOptions: createDefaultCrawlOptions({ limit: 5 }),
@ -1199,6 +1252,12 @@ describe('JinaReader', () => {
await waitFor(() => {
expect(screen.getByText(/totalPageScraped/)).toBeInTheDocument()
})
// Cleanup - resolve the promise
resolveCheckStatus!()
await waitFor(() => {
expect(screen.queryByText(/totalPageScraped/i)).not.toBeInTheDocument()
})
})
it('should construct result item from direct data response', async () => {
@ -1437,9 +1496,13 @@ describe('JinaReader', () => {
// Arrange
const mockCreateTask = createJinaReaderTask as Mock
const mockCheckStatus = checkJinaReaderTaskStatus as Mock
let resolveCheckStatus: () => void
const checkStatusPromise = new Promise((resolve) => {
resolveCheckStatus = () => resolve({ status: 'completed', current: 0, total: 0, data: [] })
})
mockCreateTask.mockResolvedValueOnce({ job_id: 'progress-job' })
mockCheckStatus.mockImplementation(() => new Promise((_resolve) => { /* pending */ })) // Never resolves
mockCheckStatus.mockImplementation(() => checkStatusPromise)
const props = createDefaultProps({
crawlOptions: createDefaultCrawlOptions({ limit: 10 }),
@ -1455,6 +1518,12 @@ describe('JinaReader', () => {
await waitFor(() => {
expect(screen.getByText(/totalPageScraped.*0\/10/)).toBeInTheDocument()
})
// Cleanup - resolve the promise
resolveCheckStatus!()
await waitFor(() => {
expect(screen.queryByText(/totalPageScraped/i)).not.toBeInTheDocument()
})
})
it('should display time consumed after crawl completion', async () => {