import { fireEvent, render, screen, waitFor } from '@testing-library/react' import React from 'react' import OnlineDrive from './index' import Header from './header' import { convertOnlineDriveData, isBucketListInitiation, isFile } from './utils' import type { OnlineDriveFile } from '@/models/pipeline' import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline' import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import type { OnlineDriveData } from '@/types/pipeline' // ========================================== // 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: string | undefined = '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 useGetDataSourceAuth - API service hook requires mocking const mockUseGetDataSourceAuth = jest.fn() jest.mock('@/service/use-datasource', () => ({ useGetDataSourceAuth: (params: any) => mockUseGetDataSourceAuth(params), })) // Mock Toast const mockToastNotify = jest.fn() jest.mock('@/app/components/base/toast', () => ({ __esModule: true, default: { notify: (...args: any[]) => mockToastNotify(...args), }, })) // Note: zustand/react/shallow useShallow is imported directly (simple utility function) // Mock store state const mockStoreState = { nextPageParameters: {} as Record, breadcrumbs: [] as string[], prefix: [] as string[], keywords: '', bucket: '', selectedFileIds: [] as string[], onlineDriveFileList: [] as OnlineDriveFile[], currentCredentialId: '', isTruncated: { current: false }, currentNextPageParametersRef: { current: {} }, setOnlineDriveFileList: jest.fn(), setKeywords: jest.fn(), setSelectedFileIds: jest.fn(), setBreadcrumbs: jest.fn(), setPrefix: jest.fn(), setBucket: jest.fn(), setHasBucket: 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 FileList component jest.mock('./file-list', () => { const MockFileList = (props: any) => (
{props.fileList?.length || 0} {props.selectedFileIds?.length || 0} {props.breadcrumbs?.join('/') || ''} {props.keywords} {props.bucket} {String(props.isLoading)} {String(props.isInPipeline)} {String(props.supportBatchUpload)} props.updateKeywords(e.target.value)} />
) return MockFileList }) // ========================================== // Test Data Builders // ========================================== const createMockNodeData = (overrides?: Partial): DataSourceNodeType => ({ title: 'Test Node', plugin_id: 'plugin-123', provider_type: 'online_drive', provider_name: 'online-drive-provider', datasource_name: 'online-drive-ds', datasource_label: 'Online Drive', datasource_parameters: {}, datasource_configurations: {}, ...overrides, } as DataSourceNodeType) const createMockOnlineDriveFile = (overrides?: Partial): OnlineDriveFile => ({ id: 'file-1', name: 'test-file.txt', size: 1024, type: OnlineDriveFileType.file, ...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 OnlineDriveProps = React.ComponentProps const createDefaultProps = (overrides?: Partial): OnlineDriveProps => ({ nodeId: 'node-1', nodeData: createMockNodeData(), onCredentialChange: jest.fn(), isInPipeline: false, supportBatchUpload: true, ...overrides, }) // ========================================== // Helper Functions // ========================================== const resetMockStoreState = () => { mockStoreState.nextPageParameters = {} mockStoreState.breadcrumbs = [] mockStoreState.prefix = [] mockStoreState.keywords = '' mockStoreState.bucket = '' mockStoreState.selectedFileIds = [] mockStoreState.onlineDriveFileList = [] mockStoreState.currentCredentialId = '' mockStoreState.isTruncated = { current: false } mockStoreState.currentNextPageParametersRef = { current: {} } mockStoreState.setOnlineDriveFileList = jest.fn() mockStoreState.setKeywords = jest.fn() mockStoreState.setSelectedFileIds = jest.fn() mockStoreState.setBreadcrumbs = jest.fn() mockStoreState.setPrefix = jest.fn() mockStoreState.setBucket = jest.fn() mockStoreState.setHasBucket = jest.fn() } // ========================================== // Test Suites // ========================================== describe('OnlineDrive', () => { beforeEach(() => { jest.clearAllMocks() // Reset store state resetMockStoreState() // 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() expect(screen.getByTestId('file-list')).toBeInTheDocument() }) it('should render Header with correct props', () => { // Arrange mockStoreState.currentCredentialId = 'cred-123' const props = createDefaultProps({ nodeData: createMockNodeData({ datasource_label: 'My Online Drive' }), }) // Act render() // Assert expect(screen.getByTestId('header-doc-title')).toHaveTextContent('Docs') expect(screen.getByTestId('header-plugin-name')).toHaveTextContent('My Online Drive') expect(screen.getByTestId('header-credential-id')).toHaveTextContent('cred-123') }) it('should render FileList with correct props', () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.keywords = 'search-term' mockStoreState.breadcrumbs = ['folder1', 'folder2'] mockStoreState.bucket = 'my-bucket' mockStoreState.selectedFileIds = ['file-1', 'file-2'] mockStoreState.onlineDriveFileList = [ createMockOnlineDriveFile({ id: 'file-1', name: 'file1.txt' }), createMockOnlineDriveFile({ id: 'file-2', name: 'file2.txt' }), ] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('file-list')).toBeInTheDocument() expect(screen.getByTestId('file-list-keywords')).toHaveTextContent('search-term') expect(screen.getByTestId('file-list-breadcrumbs')).toHaveTextContent('folder1/folder2') expect(screen.getByTestId('file-list-bucket')).toHaveTextContent('my-bucket') expect(screen.getByTestId('file-list-selected-count')).toHaveTextContent('2') }) it('should pass docLink with correct path to Header', () => { // Arrange const props = createDefaultProps() // Act render() // Assert expect(mockDocLink).toHaveBeenCalledWith('/guides/knowledge-base/knowledge-pipeline/authorize-data-source') }) }) // ========================================== // Props Testing // ========================================== describe('Props', () => { describe('nodeId prop', () => { it('should use nodeId in datasourceNodeRunURL for non-pipeline mode', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ nodeId: 'custom-node-id', isInPipeline: false, }) // Act render() // Assert - ssePost should be called with correct URL await waitFor(() => { expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/rag/pipelines/pipeline-123/workflows/published/datasource/nodes/custom-node-id/run'), expect.any(Object), expect.any(Object), ) }) }) it('should use nodeId in datasourceNodeRunURL for pipeline mode', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ nodeId: 'custom-node-id', isInPipeline: true, }) // Act render() // Assert - ssePost should be called with correct URL for draft await waitFor(() => { expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/rag/pipelines/pipeline-123/workflows/draft/datasource/nodes/custom-node-id/run'), expect.any(Object), expect.any(Object), ) }) }) }) describe('nodeData prop', () => { 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', }) }) it('should pass datasource_label to Header as pluginName', () => { // Arrange const nodeData = createMockNodeData({ datasource_label: 'Custom Online Drive', }) const props = createDefaultProps({ nodeData }) // Act render() // Assert expect(screen.getByTestId('header-plugin-name')).toHaveTextContent('Custom Online Drive') }) }) describe('isInPipeline prop', () => { it('should use draft URL when isInPipeline is true', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ isInPipeline: true }) // Act render() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/workflows/draft/'), expect.any(Object), expect.any(Object), ) }) }) it('should use published URL when isInPipeline is false', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ isInPipeline: false }) // Act render() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/workflows/published/'), expect.any(Object), expect.any(Object), ) }) }) it('should pass isInPipeline to FileList', () => { // Arrange const props = createDefaultProps({ isInPipeline: true }) // Act render() // Assert expect(screen.getByTestId('file-list-is-in-pipeline')).toHaveTextContent('true') }) }) describe('supportBatchUpload prop', () => { it('should pass supportBatchUpload true to FileList when supportBatchUpload is true', () => { // Arrange const props = createDefaultProps({ supportBatchUpload: true }) // Act render() // Assert expect(screen.getByTestId('file-list-support-batch')).toHaveTextContent('true') }) it('should pass supportBatchUpload false to FileList when supportBatchUpload is false', () => { // Arrange const props = createDefaultProps({ supportBatchUpload: false }) // Act render() // Assert expect(screen.getByTestId('file-list-support-batch')).toHaveTextContent('false') }) it.each([ [true, 'true'], [false, 'false'], [undefined, 'true'], // Default value ])('should handle supportBatchUpload=%s correctly', (value, expected) => { // Arrange const props = createDefaultProps({ supportBatchUpload: value }) // Act render() // Assert expect(screen.getByTestId('file-list-support-batch')).toHaveTextContent(expected) }) }) describe('onCredentialChange prop', () => { it('should call onCredentialChange with credential id', () => { // 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') }) }) }) // ========================================== // State Management Tests // ========================================== describe('State Management', () => { it('should fetch files on initial mount when fileList is empty', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.onlineDriveFileList = [] const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalled() }) }) it('should not fetch files on initial mount when fileList is not empty', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.onlineDriveFileList = [createMockOnlineDriveFile()] const props = createDefaultProps() // Act render() // Assert - Wait a bit to ensure no call is made await new Promise(resolve => setTimeout(resolve, 100)) expect(mockSsePost).not.toHaveBeenCalled() }) it('should not fetch files when currentCredentialId is empty', async () => { // Arrange mockStoreState.currentCredentialId = '' const props = createDefaultProps() // Act render() // Assert - Wait a bit to ensure no call is made await new Promise(resolve => setTimeout(resolve, 100)) expect(mockSsePost).not.toHaveBeenCalled() }) it('should show loading state during fetch', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockSsePost.mockImplementation(() => { // Never resolves to keep loading state }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(screen.getByTestId('file-list-loading')).toHaveTextContent('true') }) }) it('should update file list on successful fetch', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const mockFiles = [ { id: 'file-1', name: 'file1.txt', type: 'file' as const }, { id: 'file-2', name: 'file2.txt', type: 'file' as const }, ] mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeCompleted({ data: [{ bucket: '', files: mockFiles, is_truncated: false, next_page_parameters: {}, }], time_consuming: 1.0, }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockStoreState.setOnlineDriveFileList).toHaveBeenCalled() }) }) it('should show error toast on fetch error', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const errorMessage = 'Failed to fetch files' mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeError({ error: errorMessage, }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: errorMessage, }) }) }) }) // ========================================== // Memoization Logic and Dependencies Tests // ========================================== describe('Memoization Logic', () => { it('should filter files by keywords', () => { // Arrange mockStoreState.keywords = 'test' mockStoreState.onlineDriveFileList = [ createMockOnlineDriveFile({ id: '1', name: 'test-file.txt' }), createMockOnlineDriveFile({ id: '2', name: 'other-file.txt' }), createMockOnlineDriveFile({ id: '3', name: 'another-test.pdf' }), ] const props = createDefaultProps() // Act render() // Assert - filteredOnlineDriveFileList should have 2 items matching 'test' expect(screen.getByTestId('file-list-count')).toHaveTextContent('2') }) it('should return all files when keywords is empty', () => { // Arrange mockStoreState.keywords = '' mockStoreState.onlineDriveFileList = [ createMockOnlineDriveFile({ id: '1', name: 'file1.txt' }), createMockOnlineDriveFile({ id: '2', name: 'file2.txt' }), createMockOnlineDriveFile({ id: '3', name: 'file3.pdf' }), ] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('file-list-count')).toHaveTextContent('3') }) it('should filter files case-insensitively', () => { // Arrange mockStoreState.keywords = 'TEST' mockStoreState.onlineDriveFileList = [ createMockOnlineDriveFile({ id: '1', name: 'test-file.txt' }), createMockOnlineDriveFile({ id: '2', name: 'Test-Document.pdf' }), createMockOnlineDriveFile({ id: '3', name: 'other.txt' }), ] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('file-list-count')).toHaveTextContent('2') }) }) // ========================================== // Callback Stability and Memoization // ========================================== describe('Callback Stability and Memoization', () => { it('should have stable handleSetting callback', () => { // Arrange const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('header-config-btn')) // Assert expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE, }) }) it('should have stable updateKeywords that updates store', () => { // Arrange const props = createDefaultProps() render() // Act fireEvent.change(screen.getByTestId('file-list-search-input'), { target: { value: 'new-keyword' } }) // Assert expect(mockStoreState.setKeywords).toHaveBeenCalledWith('new-keyword') }) it('should have stable resetKeywords that clears keywords', () => { // Arrange mockStoreState.keywords = 'old-keyword' const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('file-list-reset-keywords')) // Assert expect(mockStoreState.setKeywords).toHaveBeenCalledWith('') }) }) // ========================================== // User Interactions and Event Handlers // ========================================== describe('User Interactions', () => { describe('File Selection', () => { it('should toggle file selection on file click', () => { // Arrange mockStoreState.selectedFileIds = [] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('file-list-select-file')) // Assert expect(mockStoreState.setSelectedFileIds).toHaveBeenCalledWith(['file-1']) }) it('should deselect file if already selected', () => { // Arrange mockStoreState.selectedFileIds = ['file-1'] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('file-list-select-file')) // Assert expect(mockStoreState.setSelectedFileIds).toHaveBeenCalledWith([]) }) it('should not select bucket type items', () => { // Arrange mockStoreState.selectedFileIds = [] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('file-list-select-bucket')) // Assert expect(mockStoreState.setSelectedFileIds).not.toHaveBeenCalled() }) it('should limit selection to one file when supportBatchUpload is false', () => { // Arrange mockStoreState.selectedFileIds = ['existing-file'] const props = createDefaultProps({ supportBatchUpload: false }) render() // Act fireEvent.click(screen.getByTestId('file-list-select-file')) // Assert - Should not add new file because there's already one selected expect(mockStoreState.setSelectedFileIds).toHaveBeenCalledWith(['existing-file']) }) it('should allow multiple selections when supportBatchUpload is true', () => { // Arrange mockStoreState.selectedFileIds = ['existing-file'] const props = createDefaultProps({ supportBatchUpload: true }) render() // Act fireEvent.click(screen.getByTestId('file-list-select-file')) // Assert expect(mockStoreState.setSelectedFileIds).toHaveBeenCalledWith(['existing-file', 'file-1']) }) }) describe('Folder Navigation', () => { it('should open folder and update breadcrumbs/prefix', () => { // Arrange mockStoreState.breadcrumbs = [] mockStoreState.prefix = [] const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('file-list-open-folder')) // Assert expect(mockStoreState.setOnlineDriveFileList).toHaveBeenCalledWith([]) expect(mockStoreState.setSelectedFileIds).toHaveBeenCalledWith([]) expect(mockStoreState.setBreadcrumbs).toHaveBeenCalledWith(['my-folder']) expect(mockStoreState.setPrefix).toHaveBeenCalledWith(['folder-1']) }) it('should open bucket and set bucket name', () => { // Arrange const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('file-list-open-bucket')) // Assert expect(mockStoreState.setOnlineDriveFileList).toHaveBeenCalledWith([]) expect(mockStoreState.setBucket).toHaveBeenCalledWith('my-bucket') }) it('should not navigate when opening a file', () => { // Arrange const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('file-list-open-file')) // Assert - No navigation functions should be called expect(mockStoreState.setBreadcrumbs).not.toHaveBeenCalled() expect(mockStoreState.setPrefix).not.toHaveBeenCalled() expect(mockStoreState.setBucket).not.toHaveBeenCalled() }) }) describe('Credential Change', () => { it('should call onCredentialChange prop', () => { // 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') }) }) describe('Configuration', () => { it('should open account setting modal on configuration click', () => { // Arrange const props = createDefaultProps() render() // Act fireEvent.click(screen.getByTestId('header-config-btn')) // Assert expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE, }) }) }) }) // ========================================== // Side Effects and Cleanup Tests // ========================================== describe('Side Effects and Cleanup', () => { it('should fetch files when nextPageParameters changes after initial mount', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.onlineDriveFileList = [createMockOnlineDriveFile()] const props = createDefaultProps() const { rerender } = render() // Act - Simulate nextPageParameters change by re-rendering with updated state mockStoreState.nextPageParameters = { page: 2 } rerender() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalled() }) }) it('should fetch files when prefix changes after initial mount', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.onlineDriveFileList = [createMockOnlineDriveFile()] const props = createDefaultProps() const { rerender } = render() // Act - Simulate prefix change by re-rendering with updated state mockStoreState.prefix = ['folder1'] rerender() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalled() }) }) it('should fetch files when bucket changes after initial mount', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.onlineDriveFileList = [createMockOnlineDriveFile()] const props = createDefaultProps() const { rerender } = render() // Act - Simulate bucket change by re-rendering with updated state mockStoreState.bucket = 'new-bucket' rerender() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalled() }) }) it('should fetch files when currentCredentialId changes after initial mount', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.onlineDriveFileList = [createMockOnlineDriveFile()] const props = createDefaultProps() const { rerender } = render() // Act - Simulate credential change by re-rendering with updated state mockStoreState.currentCredentialId = 'cred-2' rerender() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalled() }) }) it('should not fetch files concurrently (debounce)', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' let resolveFirst: () => void const firstPromise = new Promise((resolve) => { resolveFirst = resolve }) mockSsePost.mockImplementationOnce((url, options, callbacks) => { firstPromise.then(() => { callbacks.onDataSourceNodeCompleted({ data: [{ bucket: '', files: [], is_truncated: false, next_page_parameters: {} }], time_consuming: 1.0, }) }) }) const props = createDefaultProps() // Act render() // Try to trigger another fetch while first is loading mockStoreState.prefix = ['folder1'] // Assert - Only one call should be made initially due to isLoadingRef guard expect(mockSsePost).toHaveBeenCalledTimes(1) // Cleanup resolveFirst!() }) }) // ========================================== // API Calls Mocking Tests // ========================================== describe('API Calls', () => { it('should call ssePost with correct parameters', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.prefix = ['folder1'] mockStoreState.bucket = 'my-bucket' mockStoreState.nextPageParameters = { cursor: 'abc' } const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalledWith( expect.any(String), { body: { inputs: { prefix: 'folder1', bucket: 'my-bucket', next_page_parameters: { cursor: 'abc' }, max_keys: 30, }, datasource_type: DatasourceType.onlineDrive, credential_id: 'cred-1', }, }, expect.objectContaining({ onDataSourceNodeCompleted: expect.any(Function), onDataSourceNodeError: expect.any(Function), }), ) }) }) it('should handle completed response and update store', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.breadcrumbs = ['folder1'] mockStoreState.bucket = 'my-bucket' const mockResponseData = [{ bucket: 'my-bucket', files: [ { id: 'file-1', name: 'file1.txt', size: 1024, type: 'file' as const }, { id: 'file-2', name: 'file2.txt', size: 2048, type: 'file' as const }, ], is_truncated: true, next_page_parameters: { cursor: 'next-cursor' }, }] mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeCompleted({ data: mockResponseData, time_consuming: 1.5, }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockStoreState.setOnlineDriveFileList).toHaveBeenCalled() expect(mockStoreState.setHasBucket).toHaveBeenCalledWith(true) expect(mockStoreState.isTruncated.current).toBe(true) expect(mockStoreState.currentNextPageParametersRef.current).toEqual({ cursor: 'next-cursor' }) }) }) it('should handle error response and show toast', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const errorMessage = 'Access denied' mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeError({ error: errorMessage, }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockToastNotify).toHaveBeenCalledWith({ type: 'error', message: errorMessage, }) }) }) }) // ========================================== // Edge Cases and Error Handling // ========================================== describe('Edge Cases and Error Handling', () => { it('should handle empty credentials list', () => { // Arrange mockUseGetDataSourceAuth.mockReturnValue({ data: { result: [] }, }) const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('header-credentials-count')).toHaveTextContent('0') }) it('should handle undefined credentials data', () => { // Arrange mockUseGetDataSourceAuth.mockReturnValue({ data: undefined, }) const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('header-credentials-count')).toHaveTextContent('0') }) it('should handle undefined pipelineId', async () => { // Arrange mockPipelineId = undefined mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps() // Act render() // Assert - Should still attempt to call ssePost with undefined in URL await waitFor(() => { expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining('/rag/pipelines/undefined/'), expect.any(Object), expect.any(Object), ) }) }) it('should handle empty file list', () => { // Arrange mockStoreState.onlineDriveFileList = [] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('file-list-count')).toHaveTextContent('0') }) it('should handle empty breadcrumbs', () => { // Arrange mockStoreState.breadcrumbs = [] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('file-list-breadcrumbs')).toHaveTextContent('') }) it('should handle empty bucket', () => { // Arrange mockStoreState.bucket = '' const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('file-list-bucket')).toHaveTextContent('') }) it('should handle special characters in keywords', () => { // Arrange mockStoreState.keywords = 'test.file[1]' mockStoreState.onlineDriveFileList = [ createMockOnlineDriveFile({ id: '1', name: 'test.file[1].txt' }), createMockOnlineDriveFile({ id: '2', name: 'other.txt' }), ] const props = createDefaultProps() // Act render() // Assert - Should find file with special characters expect(screen.getByTestId('file-list-count')).toHaveTextContent('1') }) it('should handle very long file names', () => { // Arrange const longName = `${'a'.repeat(500)}.txt` mockStoreState.onlineDriveFileList = [ createMockOnlineDriveFile({ id: '1', name: longName }), ] const props = createDefaultProps() // Act render() // Assert expect(screen.getByTestId('file-list-count')).toHaveTextContent('1') }) it('should handle bucket list initiation response', async () => { // Arrange mockStoreState.currentCredentialId = 'cred-1' mockStoreState.bucket = '' mockStoreState.prefix = [] const mockBucketResponse = [ { bucket: 'bucket-1', files: [], is_truncated: false, next_page_parameters: {} }, { bucket: 'bucket-2', files: [], is_truncated: false, next_page_parameters: {} }, ] mockSsePost.mockImplementation((url, options, callbacks) => { callbacks.onDataSourceNodeCompleted({ data: mockBucketResponse, time_consuming: 1.0, }) }) const props = createDefaultProps() // Act render() // Assert await waitFor(() => { expect(mockStoreState.setHasBucket).toHaveBeenCalledWith(true) }) }) }) // ========================================== // All Prop Variations Tests // ========================================== 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 isInPipeline=%s and supportBatchUpload=%s', (propVariation) => { // Arrange const props = createDefaultProps(propVariation) // Act render() // Assert expect(screen.getByTestId('header')).toBeInTheDocument() expect(screen.getByTestId('file-list')).toBeInTheDocument() expect(screen.getByTestId('file-list-is-in-pipeline')).toHaveTextContent(String(propVariation.isInPipeline)) expect(screen.getByTestId('file-list-support-batch')).toHaveTextContent(String(propVariation.supportBatchUpload)) }) it.each([ { nodeId: 'node-a', expectedUrlPart: 'nodes/node-a/run' }, { nodeId: 'node-b', expectedUrlPart: 'nodes/node-b/run' }, { nodeId: '123-456', expectedUrlPart: 'nodes/123-456/run' }, ])('should use correct URL for nodeId=%s', async ({ nodeId, expectedUrlPart }) => { // Arrange mockStoreState.currentCredentialId = 'cred-1' const props = createDefaultProps({ nodeId }) // Act render() // Assert await waitFor(() => { expect(mockSsePost).toHaveBeenCalledWith( expect.stringContaining(expectedUrlPart), expect.any(Object), expect.any(Object), ) }) }) it.each([ { pluginId: 'plugin-a', providerName: 'provider-a' }, { pluginId: 'plugin-b', providerName: 'provider-b' }, { pluginId: '', providerName: '' }, ])('should call useGetDataSourceAuth with pluginId=%s and providerName=%s', ({ pluginId, providerName }) => { // Arrange const props = createDefaultProps({ nodeData: createMockNodeData({ plugin_id: pluginId, provider_name: providerName, }), }) // Act render() // Assert expect(mockUseGetDataSourceAuth).toHaveBeenCalledWith({ pluginId, provider: providerName, }) }) }) }) // ========================================== // Header Component Tests // ========================================== describe('Header', () => { const createHeaderProps = (overrides?: Partial>) => ({ onClickConfiguration: jest.fn(), docTitle: 'Documentation', docLink: 'https://docs.example.com/guide', ...overrides, }) beforeEach(() => { jest.clearAllMocks() }) describe('Rendering', () => { it('should render without crashing', () => { // Arrange const props = createHeaderProps() // Act render(
) // Assert expect(screen.getByText('Documentation')).toBeInTheDocument() }) it('should render doc link with correct href', () => { // Arrange const props = createHeaderProps({ docLink: 'https://custom-docs.com/path', docTitle: 'Custom Docs', }) // Act render(
) // Assert const link = screen.getByRole('link') expect(link).toHaveAttribute('href', 'https://custom-docs.com/path') expect(link).toHaveAttribute('target', '_blank') expect(link).toHaveAttribute('rel', 'noopener noreferrer') }) it('should render doc title text', () => { // Arrange const props = createHeaderProps({ docTitle: 'My Documentation Title' }) // Act render(
) // Assert expect(screen.getByText('My Documentation Title')).toBeInTheDocument() }) it('should render configuration button', () => { // Arrange const props = createHeaderProps() // Act render(
) // Assert expect(screen.getByRole('button')).toBeInTheDocument() }) }) describe('Props', () => { describe('docTitle prop', () => { it.each([ 'Getting Started', 'API Reference', 'Installation Guide', '', ])('should render docTitle="%s"', (docTitle) => { // Arrange const props = createHeaderProps({ docTitle }) // Act render(
) // Assert if (docTitle) expect(screen.getByText(docTitle)).toBeInTheDocument() }) }) describe('docLink prop', () => { it.each([ 'https://docs.example.com', 'https://docs.example.com/path/to/page', '/relative/path', ])('should set href to "%s"', (docLink) => { // Arrange const props = createHeaderProps({ docLink }) // Act render(
) // Assert expect(screen.getByRole('link')).toHaveAttribute('href', docLink) }) }) describe('onClickConfiguration prop', () => { it('should call onClickConfiguration when configuration icon is clicked', () => { // Arrange const mockOnClickConfiguration = jest.fn() const props = createHeaderProps({ onClickConfiguration: mockOnClickConfiguration }) // Act render(
) const configIcon = screen.getByRole('button').querySelector('svg') fireEvent.click(configIcon!) // Assert expect(mockOnClickConfiguration).toHaveBeenCalledTimes(1) }) it('should not throw when onClickConfiguration is undefined', () => { // Arrange const props = createHeaderProps({ onClickConfiguration: undefined }) // Act & Assert expect(() => render(
)).not.toThrow() }) }) }) describe('Accessibility', () => { it('should have accessible link with title attribute', () => { // Arrange const props = createHeaderProps({ docTitle: 'Accessible Title' }) // Act render(
) // Assert const titleSpan = screen.getByTitle('Accessible Title') expect(titleSpan).toBeInTheDocument() }) }) }) // ========================================== // Utils Tests // ========================================== describe('utils', () => { // ========================================== // isFile Tests // ========================================== describe('isFile', () => { it('should return true for file type', () => { // Act & Assert expect(isFile('file')).toBe(true) }) it('should return false for folder type', () => { // Act & Assert expect(isFile('folder')).toBe(false) }) it.each([ ['file', true], ['folder', false], ] as const)('isFile(%s) should return %s', (type, expected) => { // Act & Assert expect(isFile(type)).toBe(expected) }) }) // ========================================== // isBucketListInitiation Tests // ========================================== describe('isBucketListInitiation', () => { it('should return false when bucket is not empty', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [], is_truncated: false, next_page_parameters: {} }, ] // Act & Assert expect(isBucketListInitiation(data, [], 'existing-bucket')).toBe(false) }) it('should return false when prefix is not empty', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [], is_truncated: false, next_page_parameters: {} }, ] // Act & Assert expect(isBucketListInitiation(data, ['folder1'], '')).toBe(false) }) it('should return false when data items have no bucket', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: '', files: [{ id: '1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: false, next_page_parameters: {} }, ] // Act & Assert expect(isBucketListInitiation(data, [], '')).toBe(false) }) it('should return true for multiple buckets with no prefix and bucket', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'bucket-1', files: [], is_truncated: false, next_page_parameters: {} }, { bucket: 'bucket-2', files: [], is_truncated: false, next_page_parameters: {} }, ] // Act & Assert expect(isBucketListInitiation(data, [], '')).toBe(true) }) it('should return true for single bucket with no files, no prefix, and no bucket', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [], is_truncated: false, next_page_parameters: {} }, ] // Act & Assert expect(isBucketListInitiation(data, [], '')).toBe(true) }) it('should return false for single bucket with files', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: '1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: false, next_page_parameters: {} }, ] // Act & Assert expect(isBucketListInitiation(data, [], '')).toBe(false) }) it('should return false for empty data array', () => { // Arrange const data: OnlineDriveData[] = [] // Act & Assert expect(isBucketListInitiation(data, [], '')).toBe(false) }) }) // ========================================== // convertOnlineDriveData Tests // ========================================== describe('convertOnlineDriveData', () => { describe('Empty data handling', () => { it('should return empty result for empty data array', () => { // Arrange const data: OnlineDriveData[] = [] // Act const result = convertOnlineDriveData(data, [], '') // Assert expect(result).toEqual({ fileList: [], isTruncated: false, nextPageParameters: {}, hasBucket: false, }) }) }) describe('Bucket list initiation', () => { it('should convert multiple buckets to bucket file list', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'bucket-1', files: [], is_truncated: false, next_page_parameters: {} }, { bucket: 'bucket-2', files: [], is_truncated: false, next_page_parameters: {} }, { bucket: 'bucket-3', files: [], is_truncated: false, next_page_parameters: {} }, ] // Act const result = convertOnlineDriveData(data, [], '') // Assert expect(result.fileList).toHaveLength(3) expect(result.fileList[0]).toEqual({ id: 'bucket-1', name: 'bucket-1', type: OnlineDriveFileType.bucket, }) expect(result.fileList[1]).toEqual({ id: 'bucket-2', name: 'bucket-2', type: OnlineDriveFileType.bucket, }) expect(result.fileList[2]).toEqual({ id: 'bucket-3', name: 'bucket-3', type: OnlineDriveFileType.bucket, }) expect(result.hasBucket).toBe(true) expect(result.isTruncated).toBe(false) expect(result.nextPageParameters).toEqual({}) }) it('should convert single bucket with no files to bucket list', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [], is_truncated: false, next_page_parameters: {} }, ] // Act const result = convertOnlineDriveData(data, [], '') // Assert expect(result.fileList).toHaveLength(1) expect(result.fileList[0]).toEqual({ id: 'my-bucket', name: 'my-bucket', type: OnlineDriveFileType.bucket, }) expect(result.hasBucket).toBe(true) }) }) describe('File list conversion', () => { it('should convert files correctly', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [ { id: 'file-1', name: 'document.pdf', size: 1024, type: 'file' }, { id: 'file-2', name: 'image.png', size: 2048, type: 'file' }, ], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, ['folder1'], 'my-bucket') // Assert expect(result.fileList).toHaveLength(2) expect(result.fileList[0]).toEqual({ id: 'file-1', name: 'document.pdf', size: 1024, type: OnlineDriveFileType.file, }) expect(result.fileList[1]).toEqual({ id: 'file-2', name: 'image.png', size: 2048, type: OnlineDriveFileType.file, }) expect(result.hasBucket).toBe(true) }) it('should convert folders correctly without size', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [ { id: 'folder-1', name: 'Documents', size: 0, type: 'folder' }, { id: 'folder-2', name: 'Images', size: 0, type: 'folder' }, ], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.fileList).toHaveLength(2) expect(result.fileList[0]).toEqual({ id: 'folder-1', name: 'Documents', size: undefined, type: OnlineDriveFileType.folder, }) expect(result.fileList[1]).toEqual({ id: 'folder-2', name: 'Images', size: undefined, type: OnlineDriveFileType.folder, }) }) it('should handle mixed files and folders', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [ { id: 'folder-1', name: 'Documents', size: 0, type: 'folder' }, { id: 'file-1', name: 'readme.txt', size: 256, type: 'file' }, { id: 'folder-2', name: 'Images', size: 0, type: 'folder' }, { id: 'file-2', name: 'data.json', size: 512, type: 'file' }, ], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.fileList).toHaveLength(4) expect(result.fileList[0].type).toBe(OnlineDriveFileType.folder) expect(result.fileList[1].type).toBe(OnlineDriveFileType.file) expect(result.fileList[2].type).toBe(OnlineDriveFileType.folder) expect(result.fileList[3].type).toBe(OnlineDriveFileType.file) }) }) describe('Truncation and pagination', () => { it('should return isTruncated true when data is truncated', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: 'file-1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: true, next_page_parameters: { cursor: 'next-cursor' }, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.isTruncated).toBe(true) expect(result.nextPageParameters).toEqual({ cursor: 'next-cursor' }) }) it('should return isTruncated false when not truncated', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: 'file-1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.isTruncated).toBe(false) expect(result.nextPageParameters).toEqual({}) }) it('should handle undefined is_truncated', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: 'file-1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: undefined as any, next_page_parameters: undefined as any, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.isTruncated).toBe(false) expect(result.nextPageParameters).toEqual({}) }) }) describe('hasBucket flag', () => { it('should return hasBucket true when bucket exists in data', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: 'file-1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.hasBucket).toBe(true) }) it('should return hasBucket false when bucket is empty in data', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: '', files: [{ id: 'file-1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], '') // Assert expect(result.hasBucket).toBe(false) }) }) describe('Edge cases', () => { it('should handle files with zero size', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: 'file-1', name: 'empty.txt', size: 0, type: 'file' }], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.fileList[0].size).toBe(0) }) it('should handle files with very large size', () => { // Arrange const largeSize = Number.MAX_SAFE_INTEGER const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: 'file-1', name: 'large.bin', size: largeSize, type: 'file' }], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.fileList[0].size).toBe(largeSize) }) it('should handle files with special characters in name', () => { // Arrange const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [ { id: 'file-1', name: 'file[1] (copy).txt', size: 1024, type: 'file' }, { id: 'file-2', name: 'doc-with-dash_and_underscore.pdf', size: 2048, type: 'file' }, { id: 'file-3', name: 'file with spaces.txt', size: 512, type: 'file' }, ], is_truncated: false, next_page_parameters: {}, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.fileList[0].name).toBe('file[1] (copy).txt') expect(result.fileList[1].name).toBe('doc-with-dash_and_underscore.pdf') expect(result.fileList[2].name).toBe('file with spaces.txt') }) it('should handle complex next_page_parameters', () => { // Arrange const complexParams = { cursor: 'abc123', page: 2, limit: 50, nested: { key: 'value' }, } const data: OnlineDriveData[] = [ { bucket: 'my-bucket', files: [{ id: 'file-1', name: 'file.txt', size: 1024, type: 'file' }], is_truncated: true, next_page_parameters: complexParams, }, ] // Act const result = convertOnlineDriveData(data, [], 'my-bucket') // Assert expect(result.nextPageParameters).toEqual(complexParams) }) }) }) })