mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
Merge commit '657eeb65' into sandboxed-agent-rebase
Made-with: Cursor # Conflicts: # api/core/agent/cot_chat_agent_runner.py # api/core/agent/fc_agent_runner.py # api/core/memory/token_buffer_memory.py # api/core/variables/segments.py # api/core/workflow/file/file_manager.py # api/core/workflow/nodes/agent/agent_node.py # api/core/workflow/nodes/llm/llm_utils.py # api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py # api/core/workflow/workflow_entry.py # api/factories/variable_factory.py # api/pyproject.toml # api/services/variable_truncator.py # api/uv.lock # web/app/components/app/app-publisher/index.tsx # web/app/components/app/overview/settings/index.tsx # web/app/components/apps/app-card.tsx # web/app/components/apps/index.tsx # web/app/components/apps/list.tsx # web/app/components/base/chat/chat-with-history/header-in-mobile.tsx # web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx # web/app/components/base/features/new-feature-panel/file-upload/setting-content.tsx # web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx # web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx # web/app/components/base/message-log-modal/index.tsx # web/app/components/base/switch/index.tsx # web/app/components/base/tab-slider-plain/index.tsx # web/app/components/explore/try-app/app-info/index.tsx # web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx # web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/required-switch.tsx # web/app/components/workflow/nodes/llm/panel.tsx # web/contract/router.ts # web/eslint-suppressions.json # web/i18n/fa-IR/workflow.json
This commit is contained in:
@ -0,0 +1,204 @@
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||
import ConfigFirecrawlModal from './config-firecrawl-modal'
|
||||
|
||||
/**
|
||||
* ConfigFirecrawlModal Component Tests
|
||||
* Tests validation, save logic, and basic rendering for the Firecrawl configuration modal.
|
||||
*/
|
||||
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
createDataSourceApiKeyBinding: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ConfigFirecrawlModal Component', () => {
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnSaved = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Initial Rendering', () => {
|
||||
it('should render the modal with all fields and buttons', () => {
|
||||
// Act
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.firecrawl.configFirecrawl')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('datasetCreation.firecrawl.apiKeyPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('https://api.firecrawl.dev')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common\.operation\.save/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common\.operation\.cancel/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /datasetCreation\.firecrawl\.getApiKeyLinkText/i })).toHaveAttribute('href', 'https://www.firecrawl.dev/account')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Interactions', () => {
|
||||
it('should update state when input fields change', async () => {
|
||||
// Arrange
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
const apiKeyInput = screen.getByPlaceholderText('datasetCreation.firecrawl.apiKeyPlaceholder')
|
||||
const baseUrlInput = screen.getByPlaceholderText('https://api.firecrawl.dev')
|
||||
|
||||
// Act
|
||||
fireEvent.change(apiKeyInput, { target: { value: 'firecrawl-key' } })
|
||||
fireEvent.change(baseUrlInput, { target: { value: 'https://custom.firecrawl.dev' } })
|
||||
|
||||
// Assert
|
||||
expect(apiKeyInput).toHaveValue('firecrawl-key')
|
||||
expect(baseUrlInput).toHaveValue('https://custom.firecrawl.dev')
|
||||
})
|
||||
|
||||
it('should call onCancel when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.cancel/i }))
|
||||
|
||||
// Assert
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should show error when saving without API Key', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.errorMsg.fieldRequired:{"field":"API Key"}')).toBeInTheDocument()
|
||||
})
|
||||
expect(createDataSourceApiKeyBinding).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error for invalid Base URL format', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
const baseUrlInput = screen.getByPlaceholderText('https://api.firecrawl.dev')
|
||||
|
||||
// Act
|
||||
await user.type(baseUrlInput, 'ftp://invalid-url.com')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.errorMsg.urlError')).toBeInTheDocument()
|
||||
})
|
||||
expect(createDataSourceApiKeyBinding).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Saving Logic', () => {
|
||||
it('should save successfully with valid API Key and custom URL', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' })
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.firecrawl.apiKeyPlaceholder'), 'valid-key')
|
||||
await user.type(screen.getByPlaceholderText('https://api.firecrawl.dev'), 'http://my-firecrawl.com')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledWith({
|
||||
category: 'website',
|
||||
provider: 'firecrawl',
|
||||
credentials: {
|
||||
auth_type: 'bearer',
|
||||
config: {
|
||||
api_key: 'valid-key',
|
||||
base_url: 'http://my-firecrawl.com',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.api.success')).toBeInTheDocument()
|
||||
expect(mockOnSaved).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should use default Base URL if none is provided during save', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' })
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.firecrawl.apiKeyPlaceholder'), 'test-key')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledWith(expect.objectContaining({
|
||||
credentials: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
base_url: 'https://api.firecrawl.dev',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore multiple save clicks while saving is in progress', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
let resolveSave: (value: CommonResponse) => void
|
||||
const savePromise = new Promise<CommonResponse>((resolve) => {
|
||||
resolveSave = resolve
|
||||
})
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockReturnValue(savePromise)
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.firecrawl.apiKeyPlaceholder'), 'test-key')
|
||||
const saveBtn = screen.getByRole('button', { name: /common\.operation\.save/i })
|
||||
|
||||
// Act
|
||||
await user.click(saveBtn)
|
||||
await user.click(saveBtn)
|
||||
|
||||
// Assert
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Cleanup
|
||||
resolveSave!({ result: 'success' })
|
||||
await waitFor(() => expect(mockOnSaved).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('should accept base_url starting with https://', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' })
|
||||
render(<ConfigFirecrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.firecrawl.apiKeyPlaceholder'), 'test-key')
|
||||
await user.type(screen.getByPlaceholderText('https://api.firecrawl.dev'), 'https://secure-firecrawl.com')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledWith(expect.objectContaining({
|
||||
credentials: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
base_url: 'https://secure-firecrawl.com',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,138 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||
import ConfigJinaReaderModal from './config-jina-reader-modal'
|
||||
|
||||
/**
|
||||
* ConfigJinaReaderModal Component Tests
|
||||
* Tests validation, save logic, and basic rendering for the Jina Reader configuration modal.
|
||||
*/
|
||||
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
createDataSourceApiKeyBinding: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ConfigJinaReaderModal Component', () => {
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnSaved = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Initial Rendering', () => {
|
||||
it('should render the modal with API Key field and buttons', () => {
|
||||
// Act
|
||||
render(<ConfigJinaReaderModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.jinaReader.configJinaReader')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('datasetCreation.jinaReader.apiKeyPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common\.operation\.save/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common\.operation\.cancel/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /datasetCreation\.jinaReader\.getApiKeyLinkText/i })).toHaveAttribute('href', 'https://jina.ai/reader/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Interactions', () => {
|
||||
it('should update state when API Key field changes', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigJinaReaderModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
const apiKeyInput = screen.getByPlaceholderText('datasetCreation.jinaReader.apiKeyPlaceholder')
|
||||
|
||||
// Act
|
||||
await user.type(apiKeyInput, 'jina-test-key')
|
||||
|
||||
// Assert
|
||||
expect(apiKeyInput).toHaveValue('jina-test-key')
|
||||
})
|
||||
|
||||
it('should call onCancel when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigJinaReaderModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.cancel/i }))
|
||||
|
||||
// Assert
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should show error when saving without API Key', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigJinaReaderModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.errorMsg.fieldRequired:{"field":"API Key"}')).toBeInTheDocument()
|
||||
})
|
||||
expect(createDataSourceApiKeyBinding).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Saving Logic', () => {
|
||||
it('should save successfully with valid API Key', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' })
|
||||
render(<ConfigJinaReaderModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
const apiKeyInput = screen.getByPlaceholderText('datasetCreation.jinaReader.apiKeyPlaceholder')
|
||||
|
||||
// Act
|
||||
await user.type(apiKeyInput, 'valid-jina-key')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledWith({
|
||||
category: 'website',
|
||||
provider: DataSourceProvider.jinaReader,
|
||||
credentials: {
|
||||
auth_type: 'bearer',
|
||||
config: {
|
||||
api_key: 'valid-jina-key',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.api.success')).toBeInTheDocument()
|
||||
expect(mockOnSaved).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore multiple save clicks while saving is in progress', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
let resolveSave: (value: { result: 'success' }) => void
|
||||
const savePromise = new Promise<{ result: 'success' }>((resolve) => {
|
||||
resolveSave = resolve
|
||||
})
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockReturnValue(savePromise)
|
||||
render(<ConfigJinaReaderModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.jinaReader.apiKeyPlaceholder'), 'test-key')
|
||||
const saveBtn = screen.getByRole('button', { name: /common\.operation\.save/i })
|
||||
|
||||
// Act
|
||||
await user.click(saveBtn)
|
||||
await user.click(saveBtn)
|
||||
|
||||
// Assert
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Cleanup
|
||||
resolveSave!({ result: 'success' })
|
||||
await waitFor(() => expect(mockOnSaved).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,204 @@
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
import { createDataSourceApiKeyBinding } from '@/service/datasets'
|
||||
import ConfigWatercrawlModal from './config-watercrawl-modal'
|
||||
|
||||
/**
|
||||
* ConfigWatercrawlModal Component Tests
|
||||
* Tests validation, save logic, and basic rendering for the Watercrawl configuration modal.
|
||||
*/
|
||||
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
createDataSourceApiKeyBinding: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ConfigWatercrawlModal Component', () => {
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnSaved = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Initial Rendering', () => {
|
||||
it('should render the modal with all fields and buttons', () => {
|
||||
// Act
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.watercrawl.configWatercrawl')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('datasetCreation.watercrawl.apiKeyPlaceholder')).toBeInTheDocument()
|
||||
expect(screen.getByPlaceholderText('https://app.watercrawl.dev')).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common\.operation\.save/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: /common\.operation\.cancel/i })).toBeInTheDocument()
|
||||
expect(screen.getByRole('link', { name: /datasetCreation\.watercrawl\.getApiKeyLinkText/i })).toHaveAttribute('href', 'https://app.watercrawl.dev/')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Form Interactions', () => {
|
||||
it('should update state when input fields change', async () => {
|
||||
// Arrange
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
const apiKeyInput = screen.getByPlaceholderText('datasetCreation.watercrawl.apiKeyPlaceholder')
|
||||
const baseUrlInput = screen.getByPlaceholderText('https://app.watercrawl.dev')
|
||||
|
||||
// Act
|
||||
fireEvent.change(apiKeyInput, { target: { value: 'water-key' } })
|
||||
fireEvent.change(baseUrlInput, { target: { value: 'https://custom.watercrawl.dev' } })
|
||||
|
||||
// Assert
|
||||
expect(apiKeyInput).toHaveValue('water-key')
|
||||
expect(baseUrlInput).toHaveValue('https://custom.watercrawl.dev')
|
||||
})
|
||||
|
||||
it('should call onCancel when cancel button is clicked', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.cancel/i }))
|
||||
|
||||
// Assert
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Validation', () => {
|
||||
it('should show error when saving without API Key', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.errorMsg.fieldRequired:{"field":"API Key"}')).toBeInTheDocument()
|
||||
})
|
||||
expect(createDataSourceApiKeyBinding).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error for invalid Base URL format', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
const baseUrlInput = screen.getByPlaceholderText('https://app.watercrawl.dev')
|
||||
|
||||
// Act
|
||||
await user.type(baseUrlInput, 'ftp://invalid-url.com')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.errorMsg.urlError')).toBeInTheDocument()
|
||||
})
|
||||
expect(createDataSourceApiKeyBinding).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Saving Logic', () => {
|
||||
it('should save successfully with valid API Key and custom URL', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' })
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.watercrawl.apiKeyPlaceholder'), 'valid-key')
|
||||
await user.type(screen.getByPlaceholderText('https://app.watercrawl.dev'), 'http://my-watercrawl.com')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledWith({
|
||||
category: 'website',
|
||||
provider: 'watercrawl',
|
||||
credentials: {
|
||||
auth_type: 'x-api-key',
|
||||
config: {
|
||||
api_key: 'valid-key',
|
||||
base_url: 'http://my-watercrawl.com',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('common.api.success')).toBeInTheDocument()
|
||||
expect(mockOnSaved).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should use default Base URL if none is provided during save', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' })
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.watercrawl.apiKeyPlaceholder'), 'test-api-key')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledWith(expect.objectContaining({
|
||||
credentials: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
base_url: 'https://app.watercrawl.dev',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('should ignore multiple save clicks while saving is in progress', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
let resolveSave: (value: CommonResponse) => void
|
||||
const savePromise = new Promise<CommonResponse>((resolve) => {
|
||||
resolveSave = resolve
|
||||
})
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockReturnValue(savePromise)
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.watercrawl.apiKeyPlaceholder'), 'test-api-key')
|
||||
const saveBtn = screen.getByRole('button', { name: /common\.operation\.save/i })
|
||||
|
||||
// Act
|
||||
await user.click(saveBtn)
|
||||
await user.click(saveBtn)
|
||||
|
||||
// Assert
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Cleanup
|
||||
resolveSave!({ result: 'success' })
|
||||
await waitFor(() => expect(mockOnSaved).toHaveBeenCalledTimes(1))
|
||||
})
|
||||
|
||||
it('should accept base_url starting with https://', async () => {
|
||||
const user = userEvent.setup()
|
||||
// Arrange
|
||||
vi.mocked(createDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' })
|
||||
render(<ConfigWatercrawlModal onCancel={mockOnCancel} onSaved={mockOnSaved} />)
|
||||
|
||||
// Act
|
||||
await user.type(screen.getByPlaceholderText('datasetCreation.watercrawl.apiKeyPlaceholder'), 'test-api-key')
|
||||
await user.type(screen.getByPlaceholderText('https://app.watercrawl.dev'), 'https://secure-watercrawl.com')
|
||||
await user.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(createDataSourceApiKeyBinding).toHaveBeenCalledWith(expect.objectContaining({
|
||||
credentials: expect.objectContaining({
|
||||
config: expect.objectContaining({
|
||||
base_url: 'https://secure-watercrawl.com',
|
||||
}),
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,198 @@
|
||||
import type { AppContextValue } from '@/context/app-context'
|
||||
import type { CommonResponse } from '@/models/common'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { DataSourceProvider } from '@/models/common'
|
||||
import { fetchDataSources, removeDataSourceApiKeyBinding } from '@/service/datasets'
|
||||
import DataSourceWebsite from './index'
|
||||
|
||||
/**
|
||||
* DataSourceWebsite Component Tests
|
||||
* Tests integration of multiple website scraping providers (Firecrawl, WaterCrawl, Jina Reader).
|
||||
*/
|
||||
|
||||
type DataSourcesResponse = CommonResponse & {
|
||||
sources: Array<{ id: string, provider: DataSourceProvider }>
|
||||
}
|
||||
|
||||
// Mock App Context
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock Service calls
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
fetchDataSources: vi.fn(),
|
||||
removeDataSourceApiKeyBinding: vi.fn(),
|
||||
createDataSourceApiKeyBinding: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('DataSourceWebsite Component', () => {
|
||||
const mockSources = [
|
||||
{ id: '1', provider: DataSourceProvider.fireCrawl },
|
||||
{ id: '2', provider: DataSourceProvider.waterCrawl },
|
||||
{ id: '3', provider: DataSourceProvider.jinaReader },
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useAppContext).mockReturnValue({ isCurrentWorkspaceManager: true } as unknown as AppContextValue)
|
||||
vi.mocked(fetchDataSources).mockResolvedValue({ result: 'success', sources: [] } as DataSourcesResponse)
|
||||
})
|
||||
|
||||
// Helper to render and wait for initial fetch to complete
|
||||
const renderAndWait = async (provider: DataSourceProvider) => {
|
||||
const result = render(<DataSourceWebsite provider={provider} />)
|
||||
await waitFor(() => expect(fetchDataSources).toHaveBeenCalled())
|
||||
return result
|
||||
}
|
||||
|
||||
describe('Data Initialization', () => {
|
||||
it('should fetch data sources on mount and reflect configured status', async () => {
|
||||
// Arrange
|
||||
vi.mocked(fetchDataSources).mockResolvedValue({ result: 'success', sources: mockSources } as DataSourcesResponse)
|
||||
|
||||
// Act
|
||||
await renderAndWait(DataSourceProvider.fireCrawl)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.dataSource.website.configuredCrawlers')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass readOnly status based on workspace manager permissions', async () => {
|
||||
// Arrange
|
||||
vi.mocked(useAppContext).mockReturnValue({ isCurrentWorkspaceManager: false } as unknown as AppContextValue)
|
||||
|
||||
// Act
|
||||
await renderAndWait(DataSourceProvider.fireCrawl)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.dataSource.configure')).toHaveClass('cursor-default')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Provider Specific Rendering', () => {
|
||||
it('should render correct logo and name for Firecrawl', async () => {
|
||||
// Arrange
|
||||
vi.mocked(fetchDataSources).mockResolvedValue({ result: 'success', sources: [mockSources[0]] } as DataSourcesResponse)
|
||||
|
||||
// Act
|
||||
await renderAndWait(DataSourceProvider.fireCrawl)
|
||||
|
||||
// Assert
|
||||
expect(await screen.findByText('Firecrawl')).toBeInTheDocument()
|
||||
expect(screen.getByText('🔥')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render correct logo and name for WaterCrawl', async () => {
|
||||
// Arrange
|
||||
vi.mocked(fetchDataSources).mockResolvedValue({ result: 'success', sources: [mockSources[1]] } as DataSourcesResponse)
|
||||
|
||||
// Act
|
||||
await renderAndWait(DataSourceProvider.waterCrawl)
|
||||
|
||||
// Assert
|
||||
const elements = await screen.findAllByText('WaterCrawl')
|
||||
expect(elements.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('should render correct logo and name for Jina Reader', async () => {
|
||||
// Arrange
|
||||
vi.mocked(fetchDataSources).mockResolvedValue({ result: 'success', sources: [mockSources[2]] } as DataSourcesResponse)
|
||||
|
||||
// Act
|
||||
await renderAndWait(DataSourceProvider.jinaReader)
|
||||
|
||||
// Assert
|
||||
const elements = await screen.findAllByText('Jina Reader')
|
||||
expect(elements.length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Modal Interactions', () => {
|
||||
it('should manage opening and closing of configuration modals', async () => {
|
||||
// Arrange
|
||||
await renderAndWait(DataSourceProvider.fireCrawl)
|
||||
|
||||
// Act (Open)
|
||||
fireEvent.click(screen.getByText('common.dataSource.configure'))
|
||||
// Assert
|
||||
expect(screen.getByText('datasetCreation.firecrawl.configFirecrawl')).toBeInTheDocument()
|
||||
|
||||
// Act (Cancel)
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.cancel/i }))
|
||||
// Assert
|
||||
expect(screen.queryByText('datasetCreation.firecrawl.configFirecrawl')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should re-fetch sources after saving configuration (Watercrawl)', async () => {
|
||||
// Arrange
|
||||
await renderAndWait(DataSourceProvider.waterCrawl)
|
||||
fireEvent.click(screen.getByText('common.dataSource.configure'))
|
||||
vi.mocked(fetchDataSources).mockClear()
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('datasetCreation.watercrawl.apiKeyPlaceholder'), { target: { value: 'test-key' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(fetchDataSources).toHaveBeenCalled()
|
||||
expect(screen.queryByText('datasetCreation.watercrawl.configWatercrawl')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should re-fetch sources after saving configuration (Jina Reader)', async () => {
|
||||
// Arrange
|
||||
await renderAndWait(DataSourceProvider.jinaReader)
|
||||
fireEvent.click(screen.getByText('common.dataSource.configure'))
|
||||
vi.mocked(fetchDataSources).mockClear()
|
||||
|
||||
// Act
|
||||
fireEvent.change(screen.getByPlaceholderText('datasetCreation.jinaReader.apiKeyPlaceholder'), { target: { value: 'test-key' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: /common\.operation\.save/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(fetchDataSources).toHaveBeenCalled()
|
||||
expect(screen.queryByText('datasetCreation.jinaReader.configJinaReader')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Management Actions', () => {
|
||||
it('should handle successful data source removal with toast notification', async () => {
|
||||
// Arrange
|
||||
vi.mocked(fetchDataSources).mockResolvedValue({ result: 'success', sources: [mockSources[0]] } as DataSourcesResponse)
|
||||
vi.mocked(removeDataSourceApiKeyBinding).mockResolvedValue({ result: 'success' } as CommonResponse)
|
||||
await renderAndWait(DataSourceProvider.fireCrawl)
|
||||
await waitFor(() => expect(screen.getByText('common.dataSource.website.configuredCrawlers')).toBeInTheDocument())
|
||||
|
||||
// Act
|
||||
const removeBtn = screen.getByText('Firecrawl').parentElement?.querySelector('svg')?.parentElement
|
||||
if (removeBtn)
|
||||
fireEvent.click(removeBtn)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(removeDataSourceApiKeyBinding).toHaveBeenCalledWith('1')
|
||||
expect(screen.getByText('common.api.remove')).toBeInTheDocument()
|
||||
})
|
||||
expect(screen.queryByText('common.dataSource.website.configuredCrawlers')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should skip removal API call if no data source ID is present', async () => {
|
||||
// Arrange
|
||||
await renderAndWait(DataSourceProvider.fireCrawl)
|
||||
|
||||
// Act
|
||||
const removeBtn = screen.queryByText('Firecrawl')?.parentElement?.querySelector('svg')?.parentElement
|
||||
if (removeBtn)
|
||||
fireEvent.click(removeBtn)
|
||||
|
||||
// Assert
|
||||
expect(removeDataSourceApiKeyBinding).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user