import { fireEvent, render, screen, waitFor } from '@testing-library/react' import React from 'react' import OnlineDocuments from './index' import type { DataSourceNotionWorkspace, NotionPage } from '@/models/common' import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import { VarKindType } from '@/app/components/workflow/nodes/_base/types' // ========================================== // Mock Modules // ========================================== // Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts // Mock useDocLink - context hook requires mocking const mockDocLink = jest.fn((path?: string) => `https://docs.example.com${path || ''}`) jest.mock('@/context/i18n', () => ({ useDocLink: () => mockDocLink, })) // Mock dataset-detail context - context provider requires mocking let mockPipelineId = 'pipeline-123' jest.mock('@/context/dataset-detail', () => ({ useDatasetDetailContextWithSelector: (selector: (s: any) => any) => selector({ dataset: { pipeline_id: mockPipelineId } }), })) // Mock modal context - context provider requires mocking const mockSetShowAccountSettingModal = jest.fn() jest.mock('@/context/modal-context', () => ({ useModalContextSelector: (selector: (s: any) => any) => selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal }), })) // Mock ssePost - API service requires mocking const mockSsePost = jest.fn() jest.mock('@/service/base', () => ({ ssePost: (...args: any[]) => mockSsePost(...args), })) // Mock Toast.notify - static method that manipulates DOM, needs mocking to verify calls const mockToastNotify = jest.fn() jest.mock('@/app/components/base/toast', () => ({ __esModule: true, default: { notify: (options: any) => mockToastNotify(options), }, })) // Mock useGetDataSourceAuth - API service hook requires mocking const mockUseGetDataSourceAuth = jest.fn() jest.mock('@/service/use-datasource', () => ({ useGetDataSourceAuth: (params: any) => mockUseGetDataSourceAuth(params), })) // Note: zustand/react/shallow useShallow is imported directly (simple utility function) // Mock store const mockStoreState = { documentsData: [] as DataSourceNotionWorkspace[], searchValue: '', selectedPagesId: new Set(), currentCredentialId: '', setDocumentsData: jest.fn(), setSearchValue: jest.fn(), setSelectedPagesId: jest.fn(), setOnlineDocuments: jest.fn(), setCurrentDocument: jest.fn(), } const mockGetState = jest.fn(() => mockStoreState) const mockDataSourceStore = { getState: mockGetState } jest.mock('../store', () => ({ useDataSourceStoreWithSelector: (selector: (s: any) => any) => selector(mockStoreState), useDataSourceStore: () => mockDataSourceStore, })) // Mock Header component jest.mock('../base/header', () => { const MockHeader = (props: any) => (
{props.docTitle} {props.docLink} {props.pluginName} {props.currentCredentialId} {props.credentials?.length || 0}
) return MockHeader }) // Mock SearchInput component jest.mock('@/app/components/base/notion-page-selector/search-input', () => { const MockSearchInput = ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
onChange(e.target.value)} placeholder="Search" />
) return MockSearchInput }) // Mock PageSelector component jest.mock('./page-selector', () => { const MockPageSelector = (props: any) => (
{props.checkedIds?.size || 0} {props.searchValue} {String(props.canPreview)} {String(props.isMultipleChoice)} {props.currentCredentialId}
) return MockPageSelector }) // Mock Title component jest.mock('./title', () => { const MockTitle = ({ name }: { name: string }) => (
{name}
) return MockTitle }) // Mock Loading component jest.mock('@/app/components/base/loading', () => { const MockLoading = ({ type }: { type: string }) => (
Loading...
) return MockLoading }) // ========================================== // Test Data Builders // ========================================== const createMockNodeData = (overrides?: Partial): DataSourceNodeType => ({ title: 'Test Node', plugin_id: 'plugin-123', provider_type: 'notion', provider_name: 'notion-provider', datasource_name: 'notion-ds', datasource_label: 'Notion', datasource_parameters: {}, datasource_configurations: {}, ...overrides, } as DataSourceNodeType) const createMockPage = (overrides?: Partial): NotionPage => ({ page_id: 'page-1', page_name: 'Test Page', page_icon: null, is_bound: false, parent_id: 'root', type: 'page', workspace_id: 'workspace-1', ...overrides, }) const createMockWorkspace = (overrides?: Partial): DataSourceNotionWorkspace => ({ workspace_id: 'workspace-1', workspace_name: 'Test Workspace', workspace_icon: null, pages: [createMockPage()], ...overrides, }) const createMockCredential = (overrides?: Partial<{ id: string; name: string }>) => ({ id: 'cred-1', name: 'Test Credential', avatar_url: 'https://example.com/avatar.png', credential: {}, is_default: false, type: 'oauth2', ...overrides, }) type OnlineDocumentsProps = React.ComponentProps const createDefaultProps = (overrides?: Partial): OnlineDocumentsProps => ({ nodeId: 'node-1', nodeData: createMockNodeData(), onCredentialChange: jest.fn(), isInPipeline: false, supportBatchUpload: true, ...overrides, }) // ========================================== // Test Suites // ========================================== describe('OnlineDocuments', () => { beforeEach(() => { jest.clearAllMocks() // Reset store state mockStoreState.documentsData = [] mockStoreState.searchValue = '' mockStoreState.selectedPagesId = new Set() mockStoreState.currentCredentialId = '' mockStoreState.setDocumentsData = jest.fn() mockStoreState.setSearchValue = jest.fn() mockStoreState.setSelectedPagesId = jest.fn() mockStoreState.setOnlineDocuments = jest.fn() mockStoreState.setCurrentDocument = jest.fn() // Reset context values mockPipelineId = 'pipeline-123' mockSetShowAccountSettingModal.mockClear() // Default mock return values mockUseGetDataSourceAuth.mockReturnValue({ data: { result: [createMockCredential()] }, }) mockGetState.mockReturnValue(mockStoreState) }) // ========================================== // Rendering Tests // ========================================== describe('Rendering', () => { it('should render without crashing', () => { // Arrange const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('header')).toBeInTheDocument() }) it('should render Header with correct props', () => { // Arrange mockStoreState.currentCredentialId = 'cred-123' const props = createDefaultProps({ nodeData: createMockNodeData({ datasource_label: 'My Notion' }), }) // Act render() // Assert expect(screen.getByTestId('header-doc-title')).toHaveTextContent('Docs') expect(screen.getByTestId('header-plugin-name')).toHaveTextContent('My Notion') expect(screen.getByTestId('header-credential-id')).toHaveTextContent('cred-123') }) it('should render Loading when documentsData is empty', () => { // Arrange mockStoreState.documentsData = [] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('loading')).toBeInTheDocument() expect(screen.getByTestId('loading')).toHaveAttribute('data-type', 'app') }) it('should render PageSelector when documentsData has content', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('page-selector')).toBeInTheDocument() expect(screen.queryByTestId('loading')).not.toBeInTheDocument() }) it('should render Title with datasource_label', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps({ nodeData: createMockNodeData({ datasource_label: 'Notion Integration' }), }) // Act render() // Assert expect(screen.getByTestId('title-name')).toHaveTextContent('Notion Integration') }) it('should render SearchInput with current searchValue', () => { // Arrange mockStoreState.searchValue = 'test search' mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() // Act render() // Assert const searchInput = screen.getByTestId('search-input-field') as HTMLInputElement expect(searchInput.value).toBe('test search') }) }) // ========================================== // Props Testing // ========================================== describe('Props', () => { describe('nodeId prop', () => { it('should use nodeId in datasourceNodeRunURL for non-pipeline mode', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ nodeId: 'custom-node-id', isInPipeline: false, }) // Act render() // Assert - Effect triggers ssePost with correct URL expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/nodes/custom-node-id/run'), expect.any(Object), expect.any(Object), ) }) }) describe('nodeData prop', () => { it('should pass datasource_parameters to ssePost', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const nodeData = createMockNodeData({ datasource_parameters: { param1: { type: VarKindType.constant, value: 'value1' }, param2: { type: VarKindType.constant, value: 'value2' }, }, }) const props = createDefaultProps({ nodeData }) // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.objectContaining({ inputs: { param1: 'value1', param2: 'value2' }, }), }), expect.any(Object), ) }) it('should pass plugin_id and provider_name to useGetDataSourceAuth', () => { // Arrange const nodeData = createMockNodeData({ plugin_id: 'my-plugin-id', provider_name: 'my-provider', }) const props = createDefaultProps({ nodeData }) // Act render() // Assert expect(mockUseGetDataSourceAuth).toHaveBeenCalledWith({ pluginId: 'my-plugin-id', provider: 'my-provider', }) }) }) describe('isInPipeline prop', () => { it('should use draft URL when isInPipeline is true', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ isInPipeline: true }) // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/workflows/draft/'), expect.any(Object), expect.any(Object), ) }) it('should use published URL when isInPipeline is false', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ isInPipeline: false }) // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/workflows/published/'), expect.any(Object), expect.any(Object), ) }) it('should pass canPreview as false to PageSelector when isInPipeline is true', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps({ isInPipeline: true }) // Act render() // Assert expect(screen.getByTestId('page-selector-can-preview')).toHaveTextContent('false') }) it('should pass canPreview as true to PageSelector when isInPipeline is false', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps({ isInPipeline: false }) // Act render() // Assert expect(screen.getByTestId('page-selector-can-preview')).toHaveTextContent('true') }) }) describe('supportBatchUpload prop', () => { it('should pass isMultipleChoice as true to PageSelector when supportBatchUpload is true', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps({ supportBatchUpload: true }) // Act render() // Assert expect(screen.getByTestId('page-selector-multiple-choice')).toHaveTextContent('true') }) it('should pass isMultipleChoice as false to PageSelector when supportBatchUpload is false', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps({ supportBatchUpload: false }) // Act render() // Assert expect(screen.getByTestId('page-selector-multiple-choice')).toHaveTextContent('false') }) it.each([ [true, 'true'], [false, 'false'], [undefined, 'true'], // Default value ])('should handle supportBatchUpload=%s correctly', (value, expected) => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps({ supportBatchUpload: value }) // Act render() // Assert expect(screen.getByTestId('page-selector-multiple-choice')).toHaveTextContent(expected) }) }) describe('onCredentialChange prop', () => { it('should pass onCredentialChange to Header', () => { // Arrange const mockOnCredentialChange = jest.fn() const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange }) // Act render() fireEvent.click(screen.getByTestId('header-credential-change')) // Assert expect(mockOnCredentialChange).toHaveBeenCalledWith('new-cred-id') }) }) }) // ========================================== // Side Effects and Cleanup // ========================================== describe('Side Effects and Cleanup', () => { it('should call getOnlineDocuments when currentCredentialId changes', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps() // Act render() // Assert expect(mockSsePost).toHaveBeenCalledTimes(1) }) it('should not call getOnlineDocuments when currentCredentialId is empty', () => { // Arrange mockStoreState.currentCredentialId = '' const props = createDefaultProps() // Act render() // Assert expect(mockSsePost).not.toHaveBeenCalled() }) it('should pass correct body parameters to ssePost', () => { // Arrange mockStoreState.currentCredentialId = 'cred-123' const props = createDefaultProps() // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( expect.any(String), { body: { inputs: {}, credential_id: 'cred-123', datasource_type: 'online_document', }, }, expect.any(Object), ) }) it('should handle onDataSourceNodeCompleted callback correctly', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const mockWorkspaces = [createMockWorkspace()] mockSsePost.mockImplementation((url, options, callbacks) => { // Simulate successful response callbacks.onDataSourceNodeCompleted({ event: 'datasource_completed', data: mockWorkspaces, time_consuming: 1000, }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockStoreState.setDocumentsData).toHaveBeenCalledWith(mockWorkspaces) }) }) it('should handle onDataSourceNodeError callback correctly', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockSsePost.mockImplementation((url, options, callbacks) => { // Simulate error response callbacks.onDataSourceNodeError({ event: 'datasource_error', error: 'Something went wrong', }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: 'Something went wrong', }) }) }) it('should construct correct URL for draft workflow', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockPipelineId = 'pipeline-456' const props = createDefaultProps({ nodeId: 'node-789', isInPipeline: true, }) // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( '/rag/pipelines/pipeline-456/workflows/draft/datasource/nodes/node-789/run', expect.any(Object), expect.any(Object), ) }) it('should construct correct URL for published workflow', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockPipelineId = 'pipeline-456' const props = createDefaultProps({ nodeId: 'node-789', isInPipeline: false, }) // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( '/rag/pipelines/pipeline-456/workflows/published/datasource/nodes/node-789/run', expect.any(Object), expect.any(Object), ) }) }) // ========================================== // Callback Stability and Memoization // ========================================== describe('Callback Stability and Memoization', () => { it('should have stable handleSearchValueChange that updates store', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() render() // Act const searchInput = screen.getByTestId('search-input-field') fireEvent.change(searchInput, { target: { value: 'new search value' } }) // Assert expect(mockStoreState.setSearchValue).toHaveBeenCalledWith('new search value') }) it('should have stable handleSelectPages that updates store', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('page-selector-select-btn')) // Assert expect(mockStoreState.setSelectedPagesId).toHaveBeenCalled() expect(mockStoreState.setOnlineDocuments).toHaveBeenCalled() }) it('should have stable handlePreviewPage that updates store', () => { // Arrange const mockPages = [ createMockPage({ page_id: 'page-1', page_name: 'Page 1' }), ] mockStoreState.documentsData = [createMockWorkspace({ pages: mockPages })] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('page-selector-preview-btn')) // Assert expect(mockStoreState.setCurrentDocument).toHaveBeenCalled() }) it('should have stable handleSetting callback', () => { // Arrange const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('header-config-btn')) // Assert expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'data-source', }) }) }) // ========================================== // Memoization Logic and Dependencies // ========================================== describe('Memoization Logic and Dependencies', () => { it('should compute PagesMapAndSelectedPagesId correctly from documentsData', () => { // Arrange const mockPages = [ createMockPage({ page_id: 'page-1', page_name: 'Page 1' }), createMockPage({ page_id: 'page-2', page_name: 'Page 2' }), ] mockStoreState.documentsData = [ createMockWorkspace({ workspace_id: 'ws-1', pages: mockPages }), ] const props = createDefaultProps() // Act render() // Assert - PageSelector receives the pagesMap (verified via mock) expect(screen.getByTestId('page-selector')).toBeInTheDocument() }) it('should recompute PagesMapAndSelectedPagesId when documentsData changes', () => { // Arrange const initialPages = [createMockPage({ page_id: 'page-1' })] mockStoreState.documentsData = [createMockWorkspace({ pages: initialPages })] const props = createDefaultProps() const { rerender } = render() // Act - Update documentsData const newPages = [ createMockPage({ page_id: 'page-1' }), createMockPage({ page_id: 'page-2' }), ] mockStoreState.documentsData = [createMockWorkspace({ pages: newPages })] rerender() // Assert expect(screen.getByTestId('page-selector')).toBeInTheDocument() }) it('should handle empty documentsData in PagesMapAndSelectedPagesId computation', () => { // Arrange mockStoreState.documentsData = [] const props = createDefaultProps() // Act render() // Assert - Should show loading instead of PageSelector expect(screen.getByTestId('loading')).toBeInTheDocument() }) }) // ========================================== // User Interactions and Event Handlers // ========================================== describe('User Interactions and Event Handlers', () => { it('should handle search input changes', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() render() // Act const searchInput = screen.getByTestId('search-input-field') fireEvent.change(searchInput, { target: { value: 'search query' } }) // Assert expect(mockStoreState.setSearchValue).toHaveBeenCalledWith('search query') }) it('should handle page selection', () => { // Arrange const mockPages = [ createMockPage({ page_id: 'page-1', page_name: 'Page 1' }), createMockPage({ page_id: 'page-2', page_name: 'Page 2' }), ] mockStoreState.documentsData = [createMockWorkspace({ pages: mockPages })] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('page-selector-select-btn')) // Assert expect(mockStoreState.setSelectedPagesId).toHaveBeenCalled() expect(mockStoreState.setOnlineDocuments).toHaveBeenCalled() }) it('should handle page preview', () => { // Arrange const mockPages = [createMockPage({ page_id: 'page-1', page_name: 'Page 1' })] mockStoreState.documentsData = [createMockWorkspace({ pages: mockPages })] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('page-selector-preview-btn')) // Assert expect(mockStoreState.setCurrentDocument).toHaveBeenCalled() }) it('should handle configuration button click', () => { // Arrange const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('header-config-btn')) // Assert expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'data-source', }) }) it('should handle credential change', () => { // Arrange const mockOnCredentialChange = jest.fn() const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange }) render() // Act fireEvent.click(screen.getByTestId('header-credential-change')) // Assert expect(mockOnCredentialChange).toHaveBeenCalledWith('new-cred-id') }) }) // ========================================== // API Calls Mocking // ========================================== describe('API Calls', () => { it('should call ssePost with correct parameters', () => { // Arrange mockStoreState.currentCredentialId = 'test-cred' const props = createDefaultProps({ nodeData: createMockNodeData({ datasource_parameters: { workspace: { type: VarKindType.constant, value: 'ws-123' }, database: { type: VarKindType.constant, value: 'db-456' }, }, }), }) // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( expect.any(String), { body: { inputs: { workspace: 'ws-123', database: 'db-456' }, credential_id: 'test-cred', datasource_type: 'online_document', }, }, expect.objectContaining({ onDataSourceNodeCompleted: expect.any(Function), onDataSourceNodeError: expect.any(Function), }), ) }) it('should handle successful API response', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const mockData = [createMockWorkspace()] mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeCompleted({ event: 'datasource_completed', data: mockData, time_consuming: 500, }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockStoreState.setDocumentsData).toHaveBeenCalledWith(mockData) }) }) it('should handle API error response', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeError({ event: 'datasource_error', error: 'API Error Message', }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: 'API Error Message', }) }) }) it('should use useGetDataSourceAuth with correct parameters', () => { // Arrange const nodeData = createMockNodeData({ plugin_id: 'notion-plugin', provider_name: 'notion-provider', }) const props = createDefaultProps({ nodeData }) // Act render() // Assert expect(mockUseGetDataSourceAuth).toHaveBeenCalledWith({ pluginId: 'notion-plugin', provider: 'notion-provider', }) }) it('should pass credentials from useGetDataSourceAuth to Header', () => { // Arrange const mockCredentials = [ createMockCredential({ id: 'cred-1', name: 'Credential 1' }), createMockCredential({ id: 'cred-2', name: 'Credential 2' }), ] mockUseGetDataSourceAuth.mockReturnValue({ data: { result: mockCredentials }, }) const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('header-credentials-count')).toHaveTextContent('2') }) }) // ========================================== // Edge Cases and Error Handling // ========================================== describe('Edge Cases and Error Handling', () => { it('should handle empty credentials array', () => { // Arrange mockUseGetDataSourceAuth.mockReturnValue({ data: { result: [] }, }) const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('header-credentials-count')).toHaveTextContent('0') }) it('should handle undefined dataSourceAuth result', () => { // Arrange mockUseGetDataSourceAuth.mockReturnValue({ data: { result: undefined }, }) const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('header-credentials-count')).toHaveTextContent('0') }) it('should handle null dataSourceAuth data', () => { // Arrange mockUseGetDataSourceAuth.mockReturnValue({ data: null, }) const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('header-credentials-count')).toHaveTextContent('0') }) it('should handle documentsData with empty pages array', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace({ pages: [] })] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('page-selector')).toBeInTheDocument() }) it('should handle undefined documentsData in useMemo (line 59 branch)', () => { // Arrange - Set documentsData to undefined to test the || [] fallback mockStoreState.documentsData = undefined as unknown as DataSourceNotionWorkspace[] const props = createDefaultProps() // Act render() // Assert - Should show loading when documentsData is undefined expect(screen.getByTestId('loading')).toBeInTheDocument() }) it('should handle undefined datasource_parameters (line 79 branch)', () => { // Arrange - Set datasource_parameters to undefined to test the || {} fallback mockStoreState.currentCredentialId = 'cred-1' const nodeData = createMockNodeData() // @ts-expect-error - Testing undefined case for branch coverage nodeData.datasource_parameters = undefined const props = createDefaultProps({ nodeData }) // Act render() // Assert - ssePost should be called with empty inputs expect(mockSsePost).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.objectContaining({ inputs: {}, }), }), expect.any(Object), ) }) it('should handle datasource_parameters value without value property (line 80 else branch)', () => { // Arrange - Test the else branch where value is not an object with 'value' property // This tests: typeof value === 'object' && value !== null && 'value' in value ? value.value : value // The else branch (: value) is executed when value is a primitive or object without 'value' key mockStoreState.currentCredentialId = 'cred-1' const nodeData = createMockNodeData({ datasource_parameters: { // Object without 'value' key - should use the object itself objWithoutValue: { type: VarKindType.constant, other: 'data' } as any, }, }) const props = createDefaultProps({ nodeData }) // Act render() // Assert - The object without 'value' property should be passed as-is expect(mockSsePost).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.objectContaining({ inputs: expect.objectContaining({ objWithoutValue: expect.objectContaining({ type: VarKindType.constant, other: 'data' }), }), }), }), expect.any(Object), ) }) it('should handle multiple workspaces in documentsData', () => { // Arrange mockStoreState.documentsData = [ createMockWorkspace({ workspace_id: 'ws-1', pages: [createMockPage({ page_id: 'page-1' })] }), createMockWorkspace({ workspace_id: 'ws-2', pages: [createMockPage({ page_id: 'page-2' })] }), ] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('page-selector')).toBeInTheDocument() }) it('should handle special characters in searchValue', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() render() // Act const searchInput = screen.getByTestId('search-input-field') fireEvent.change(searchInput, { target: { value: 'test' } }) // Assert expect(mockStoreState.setSearchValue).toHaveBeenCalledWith('test') }) it('should handle unicode characters in searchValue', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() render() // Act const searchInput = screen.getByTestId('search-input-field') fireEvent.change(searchInput, { target: { value: 'ๆต‹่ฏ•ๆœ็ดข ๐Ÿ”' } }) // Assert expect(mockStoreState.setSearchValue).toHaveBeenCalledWith('ๆต‹่ฏ•ๆœ็ดข ๐Ÿ”') }) it('should handle empty string currentCredentialId', () => { // Arrange mockStoreState.currentCredentialId = '' const props = createDefaultProps() // Act render() // Assert expect(mockSsePost).not.toHaveBeenCalled() }) it('should handle complex datasource_parameters with nested objects', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const nodeData = createMockNodeData({ datasource_parameters: { simple: { type: VarKindType.constant, value: 'value' }, nested: { type: VarKindType.constant, value: 'nested-value' }, }, }) const props = createDefaultProps({ nodeData }) // Act render() // Assert expect(mockSsePost).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.objectContaining({ inputs: expect.objectContaining({ simple: 'value', nested: 'nested-value', }), }), }), expect.any(Object), ) }) it('should handle undefined pipelineId gracefully', () => { // Arrange mockPipelineId = undefined as any mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps() // Act render() // Assert - Should still call ssePost with undefined in URL expect(mockSsePost).toHaveBeenCalled() }) }) // ========================================== // All Prop Variations // ========================================== describe('Prop Variations', () => { it.each([ [{ isInPipeline: true, supportBatchUpload: true }], [{ isInPipeline: true, supportBatchUpload: false }], [{ isInPipeline: false, supportBatchUpload: true }], [{ isInPipeline: false, supportBatchUpload: false }], ])('should render correctly with props %o', (propVariation) => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps(propVariation) // Act render() // Assert expect(screen.getByTestId('page-selector')).toBeInTheDocument() expect(screen.getByTestId('page-selector-can-preview')).toHaveTextContent( String(!propVariation.isInPipeline), ) expect(screen.getByTestId('page-selector-multiple-choice')).toHaveTextContent( String(propVariation.supportBatchUpload), ) }) it('should use default values for optional props', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props: OnlineDocumentsProps = { nodeId: 'node-1', nodeData: createMockNodeData(), onCredentialChange: jest.fn(), // isInPipeline and supportBatchUpload are not provided } // Act render() // Assert - Default values: isInPipeline = false, supportBatchUpload = true expect(screen.getByTestId('page-selector-can-preview')).toHaveTextContent('true') expect(screen.getByTestId('page-selector-multiple-choice')).toHaveTextContent('true') }) }) // ========================================== // Integration Tests // ========================================== describe('Integration', () => { it('should complete full workflow: load data -> search -> select -> preview', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const mockPages = [ createMockPage({ page_id: 'page-1', page_name: 'Test Page 1' }), createMockPage({ page_id: 'page-2', page_name: 'Test Page 2' }), ] const mockWorkspace = createMockWorkspace({ pages: mockPages }) mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeCompleted({ event: 'datasource_completed', data: [mockWorkspace], time_consuming: 100, }) }) // Update store state after API call mockStoreState.documentsData = [mockWorkspace] const props = createDefaultProps() render() // Assert - Data loaded and PageSelector shown await waitFor(() => { expect(mockStoreState.setDocumentsData).toHaveBeenCalled() }) // Act - Search const searchInput = screen.getByTestId('search-input-field') fireEvent.change(searchInput, { target: { value: 'Test' } }) expect(mockStoreState.setSearchValue).toHaveBeenCalledWith('Test') // Act - Select pages fireEvent.click(screen.getByTestId('page-selector-select-btn')) expect(mockStoreState.setSelectedPagesId).toHaveBeenCalled() // Act - Preview page fireEvent.click(screen.getByTestId('page-selector-preview-btn')) expect(mockStoreState.setCurrentDocument).toHaveBeenCalled() }) it('should handle error flow correctly', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeError({ event: 'datasource_error', error: 'Failed to fetch documents', }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: 'Failed to fetch documents', }) }) // Should still show loading since documentsData is empty expect(screen.getByTestId('loading')).toBeInTheDocument() }) it('should handle credential change and refetch documents', () => { // Arrange mockStoreState.currentCredentialId = 'initial-cred' const mockOnCredentialChange = jest.fn() const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange }) // Act render() // Initial fetch expect(mockSsePost).toHaveBeenCalledTimes(1) // Change credential fireEvent.click(screen.getByTestId('header-credential-change')) expect(mockOnCredentialChange).toHaveBeenCalledWith('new-cred-id') }) }) // ========================================== // Styling // ========================================== describe('Styling', () => { it('should apply correct container classes', () => { // Arrange const props = createDefaultProps() // Act const { container } = render() // Assert const rootDiv = container.firstChild as HTMLElement expect(rootDiv).toHaveClass('flex', 'flex-col', 'gap-y-2') }) it('should apply correct classes to main content container', () => { // Arrange mockStoreState.documentsData = [createMockWorkspace()] const props = createDefaultProps() // Act const { container } = render() // Assert const contentContainer = container.querySelector('.rounded-xl.border') expect(contentContainer).toBeInTheDocument() expect(contentContainer).toHaveClass('border-components-panel-border', 'bg-background-default-subtle') }) }) })