import type { Mock } from 'vitest' import type { ExternalAPIItem } from '@/models/datasets' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { createExternalKnowledgeBase } from '@/service/datasets' import ExternalKnowledgeBaseConnector from './index' // Mock next/navigation const mockRouterBack = vi.fn() const mockReplace = vi.fn() vi.mock('next/navigation', () => ({ useRouter: () => ({ back: mockRouterBack, replace: mockReplace, push: vi.fn(), refresh: vi.fn(), }), })) // Mock useDocLink hook vi.mock('@/context/i18n', () => ({ useDocLink: () => (path?: string) => `https://docs.dify.ai/en${path || ''}`, })) // Mock toast context const mockNotify = vi.fn() vi.mock('@/app/components/base/toast', () => ({ useToastContext: () => ({ notify: mockNotify, }), })) // Mock modal context vi.mock('@/context/modal-context', () => ({ useModalContext: () => ({ setShowExternalKnowledgeAPIModal: vi.fn(), }), })) // Mock API service vi.mock('@/service/datasets', () => ({ createExternalKnowledgeBase: vi.fn(), })) // Factory function to create mock ExternalAPIItem const createMockExternalAPIItem = (overrides: Partial = {}): ExternalAPIItem => ({ id: 'api-default', tenant_id: 'tenant-1', name: 'Default API', description: 'Default API description', settings: { endpoint: 'https://api.example.com', api_key: 'test-api-key', }, dataset_bindings: [], created_by: 'user-1', created_at: '2024-01-01T00:00:00Z', ...overrides, }) // Default mock API list const createDefaultMockApiList = (): ExternalAPIItem[] => [ createMockExternalAPIItem({ id: 'api-1', name: 'Test API 1', settings: { endpoint: 'https://api1.example.com', api_key: 'key-1' }, }), createMockExternalAPIItem({ id: 'api-2', name: 'Test API 2', settings: { endpoint: 'https://api2.example.com', api_key: 'key-2' }, }), ] let mockExternalKnowledgeApiList: ExternalAPIItem[] = createDefaultMockApiList() vi.mock('@/context/external-knowledge-api-context', () => ({ useExternalKnowledgeApi: () => ({ externalKnowledgeApiList: mockExternalKnowledgeApiList, mutateExternalKnowledgeApis: vi.fn(), isLoading: false, }), })) // Suppress console.error helper const suppressConsoleError = () => vi.spyOn(console, 'error').mockImplementation(vi.fn()) // Helper to create a pending promise with external resolver function createPendingPromise() { let resolve: (value: T) => void = vi.fn() const promise = new Promise((r) => { resolve = r }) return { promise, resolve } } // Helper to fill required form fields and submit async function fillFormAndSubmit(user: ReturnType) { const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder') const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder') fireEvent.change(nameInput, { target: { value: 'Test Knowledge Base' } }) fireEvent.change(knowledgeIdInput, { target: { value: 'kb-123' } }) // Wait for button to be enabled await waitFor(() => { const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') expect(connectButton).not.toBeDisabled() }) const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') await user.click(connectButton!) } describe('ExternalKnowledgeBaseConnector', () => { beforeEach(() => { vi.clearAllMocks() mockExternalKnowledgeApiList = createDefaultMockApiList() ;(createExternalKnowledgeBase as Mock).mockResolvedValue({ id: 'new-kb-id' }) }) // Tests for rendering with real ExternalKnowledgeBaseCreate component describe('Rendering', () => { it('should render the create form with all required elements', () => { render() // Verify main title and form elements expect(screen.getByText('dataset.connectDataset')).toBeInTheDocument() expect(screen.getByText('dataset.externalKnowledgeName')).toBeInTheDocument() expect(screen.getByText('dataset.externalKnowledgeId')).toBeInTheDocument() expect(screen.getByText('dataset.retrievalSettings')).toBeInTheDocument() // Verify buttons expect(screen.getByText('dataset.externalKnowledgeForm.cancel')).toBeInTheDocument() expect(screen.getByText('dataset.externalKnowledgeForm.connect')).toBeInTheDocument() }) it('should render connect button disabled initially', () => { render() const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') expect(connectButton).toBeDisabled() }) }) // Tests for API success flow describe('API Success Flow', () => { it('should call API and show success notification when form is submitted', async () => { const user = userEvent.setup() render() await fillFormAndSubmit(user) // Verify API was called with form data await waitFor(() => { expect(createExternalKnowledgeBase).toHaveBeenCalledWith({ body: expect.objectContaining({ name: 'Test Knowledge Base', external_knowledge_id: 'kb-123', external_knowledge_api_id: 'api-1', provider: 'external', }), }) }) // Verify success notification expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'External Knowledge Base Connected Successfully', }) // Verify navigation back expect(mockRouterBack).toHaveBeenCalledTimes(1) }) it('should include retrieval settings in API call', async () => { const user = userEvent.setup() render() await fillFormAndSubmit(user) await waitFor(() => { expect(createExternalKnowledgeBase).toHaveBeenCalledWith({ body: expect.objectContaining({ external_retrieval_model: expect.objectContaining({ top_k: 4, score_threshold: 0.5, score_threshold_enabled: false, }), }), }) }) }) }) // Tests for API error flow describe('API Error Flow', () => { it('should show error notification when API fails', async () => { const user = userEvent.setup() const consoleErrorSpy = suppressConsoleError() ;(createExternalKnowledgeBase as Mock).mockRejectedValue(new Error('Network Error')) render() await fillFormAndSubmit(user) // Verify error notification await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'Failed to connect External Knowledge Base', }) }) // Verify no navigation expect(mockRouterBack).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() }) it('should show error notification when API returns invalid result', async () => { const user = userEvent.setup() const consoleErrorSpy = suppressConsoleError() ;(createExternalKnowledgeBase as Mock).mockResolvedValue({}) render() await fillFormAndSubmit(user) await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', message: 'Failed to connect External Knowledge Base', }) }) expect(mockRouterBack).not.toHaveBeenCalled() consoleErrorSpy.mockRestore() }) }) // Tests for loading state describe('Loading State', () => { it('should show loading state during API call', async () => { const user = userEvent.setup() // Create a promise that won't resolve immediately const { promise, resolve: resolvePromise } = createPendingPromise<{ id: string }>() ;(createExternalKnowledgeBase as Mock).mockReturnValue(promise) render() // Fill form const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder') const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder') fireEvent.change(nameInput, { target: { value: 'Test' } }) fireEvent.change(knowledgeIdInput, { target: { value: 'kb-1' } }) await waitFor(() => { const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') expect(connectButton).not.toBeDisabled() }) // Click connect const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') await user.click(connectButton!) // Button should show loading (the real Button component has loading prop) await waitFor(() => { expect(createExternalKnowledgeBase).toHaveBeenCalled() }) // Resolve the promise resolvePromise({ id: 'new-id' }) await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'success', message: 'External Knowledge Base Connected Successfully', }) }) }) }) // Tests for form validation (integration with real create component) describe('Form Validation', () => { it('should keep button disabled when only name is filled', () => { render() const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder') fireEvent.change(nameInput, { target: { value: 'Test' } }) const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') expect(connectButton).toBeDisabled() }) it('should keep button disabled when only knowledge id is filled', () => { render() const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder') fireEvent.change(knowledgeIdInput, { target: { value: 'kb-1' } }) const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') expect(connectButton).toBeDisabled() }) it('should enable button when all required fields are filled', async () => { render() const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder') const knowledgeIdInput = screen.getByPlaceholderText('dataset.externalKnowledgeIdPlaceholder') fireEvent.change(nameInput, { target: { value: 'Test' } }) fireEvent.change(knowledgeIdInput, { target: { value: 'kb-1' } }) await waitFor(() => { const connectButton = screen.getByText('dataset.externalKnowledgeForm.connect').closest('button') expect(connectButton).not.toBeDisabled() }) }) }) // Tests for user interactions describe('User Interactions', () => { it('should allow typing in form fields', async () => { const user = userEvent.setup() render() const nameInput = screen.getByPlaceholderText('dataset.externalKnowledgeNamePlaceholder') const descriptionInput = screen.getByPlaceholderText('dataset.externalKnowledgeDescriptionPlaceholder') await user.type(nameInput, 'My Knowledge Base') await user.type(descriptionInput, 'My Description') expect((nameInput as HTMLInputElement).value).toBe('My Knowledge Base') expect((descriptionInput as HTMLTextAreaElement).value).toBe('My Description') }) it('should handle cancel button click', async () => { const user = userEvent.setup() render() const cancelButton = screen.getByText('dataset.externalKnowledgeForm.cancel').closest('button') await user.click(cancelButton!) expect(mockReplace).toHaveBeenCalledWith('/datasets') }) it('should handle back button click', async () => { const user = userEvent.setup() render() const buttons = screen.getAllByRole('button') const backButton = buttons.find(btn => btn.classList.contains('rounded-full')) await user.click(backButton!) expect(mockReplace).toHaveBeenCalledWith('/datasets') }) }) })