mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 09:28:04 +08:00
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:
@ -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¶m=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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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¶m=2',
|
||||
includes: 'docs/**/*.md',
|
||||
})
|
||||
render(<Options payload={payload} onChange={mockOnChange} />)
|
||||
|
||||
expect(screen.getByDisplayValue('path/*/file?query=1¶m=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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user