Files
dify/web/app/components/datasets/documents/create-from-pipeline/data-source/online-drive/index.spec.tsx
2025-12-19 17:49:51 +08:00

1896 lines
60 KiB
TypeScript

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<string, any>,
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) => (
<div data-testid="header">
<span data-testid="header-doc-title">{props.docTitle}</span>
<span data-testid="header-doc-link">{props.docLink}</span>
<span data-testid="header-plugin-name">{props.pluginName}</span>
<span data-testid="header-credential-id">{props.currentCredentialId}</span>
<button data-testid="header-config-btn" onClick={props.onClickConfiguration}>Configure</button>
<button data-testid="header-credential-change" onClick={() => props.onCredentialChange('new-cred-id')}>Change Credential</button>
<span data-testid="header-credentials-count">{props.credentials?.length || 0}</span>
</div>
)
return MockHeader
})
// Mock FileList component
jest.mock('./file-list', () => {
const MockFileList = (props: any) => (
<div data-testid="file-list">
<span data-testid="file-list-count">{props.fileList?.length || 0}</span>
<span data-testid="file-list-selected-count">{props.selectedFileIds?.length || 0}</span>
<span data-testid="file-list-breadcrumbs">{props.breadcrumbs?.join('/') || ''}</span>
<span data-testid="file-list-keywords">{props.keywords}</span>
<span data-testid="file-list-bucket">{props.bucket}</span>
<span data-testid="file-list-loading">{String(props.isLoading)}</span>
<span data-testid="file-list-is-in-pipeline">{String(props.isInPipeline)}</span>
<span data-testid="file-list-support-batch">{String(props.supportBatchUpload)}</span>
<input
data-testid="file-list-search-input"
onChange={e => props.updateKeywords(e.target.value)}
/>
<button data-testid="file-list-reset-keywords" onClick={props.resetKeywords}>Reset</button>
<button
data-testid="file-list-select-file"
onClick={() => {
const file: OnlineDriveFile = { id: 'file-1', name: 'test.txt', type: OnlineDriveFileType.file }
props.handleSelectFile(file)
}}
>
Select File
</button>
<button
data-testid="file-list-select-bucket"
onClick={() => {
const file: OnlineDriveFile = { id: 'bucket-1', name: 'my-bucket', type: OnlineDriveFileType.bucket }
props.handleSelectFile(file)
}}
>
Select Bucket
</button>
<button
data-testid="file-list-open-folder"
onClick={() => {
const file: OnlineDriveFile = { id: 'folder-1', name: 'my-folder', type: OnlineDriveFileType.folder }
props.handleOpenFolder(file)
}}
>
Open Folder
</button>
<button
data-testid="file-list-open-bucket"
onClick={() => {
const file: OnlineDriveFile = { id: 'bucket-1', name: 'my-bucket', type: OnlineDriveFileType.bucket }
props.handleOpenFolder(file)
}}
>
Open Bucket
</button>
<button
data-testid="file-list-open-file"
onClick={() => {
const file: OnlineDriveFile = { id: 'file-1', name: 'test.txt', type: OnlineDriveFileType.file }
props.handleOpenFolder(file)
}}
>
Open File
</button>
</div>
)
return MockFileList
})
// ==========================================
// Test Data Builders
// ==========================================
const createMockNodeData = (overrides?: Partial<DataSourceNodeType>): 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>): 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<typeof OnlineDrive>
const createDefaultProps = (overrides?: Partial<OnlineDriveProps>): 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// Act - Simulate nextPageParameters change by re-rendering with updated state
mockStoreState.nextPageParameters = { page: 2 }
rerender(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// Act - Simulate prefix change by re-rendering with updated state
mockStoreState.prefix = ['folder1']
rerender(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// Act - Simulate bucket change by re-rendering with updated state
mockStoreState.bucket = 'new-bucket'
rerender(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// Act - Simulate credential change by re-rendering with updated state
mockStoreState.currentCredentialId = 'cred-2'
rerender(<OnlineDrive {...props} />)
// 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<void>((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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// Assert
expect(screen.getByTestId('file-list-count')).toHaveTextContent('0')
})
it('should handle empty breadcrumbs', () => {
// Arrange
mockStoreState.breadcrumbs = []
const props = createDefaultProps()
// Act
render(<OnlineDrive {...props} />)
// Assert
expect(screen.getByTestId('file-list-breadcrumbs')).toHaveTextContent('')
})
it('should handle empty bucket', () => {
// Arrange
mockStoreState.bucket = ''
const props = createDefaultProps()
// Act
render(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// 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(<OnlineDrive {...props} />)
// Assert
expect(mockUseGetDataSourceAuth).toHaveBeenCalledWith({
pluginId,
provider: providerName,
})
})
})
})
// ==========================================
// Header Component Tests
// ==========================================
describe('Header', () => {
const createHeaderProps = (overrides?: Partial<React.ComponentProps<typeof Header>>) => ({
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(<Header {...props} />)
// 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(<Header {...props} />)
// 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(<Header {...props} />)
// Assert
expect(screen.getByText('My Documentation Title')).toBeInTheDocument()
})
it('should render configuration button', () => {
// Arrange
const props = createHeaderProps()
// Act
render(<Header {...props} />)
// 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(<Header {...props} />)
// 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(<Header {...props} />)
// 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(<Header {...props} />)
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(<Header {...props} />)).not.toThrow()
})
})
})
describe('Accessibility', () => {
it('should have accessible link with title attribute', () => {
// Arrange
const props = createHeaderProps({ docTitle: 'Accessible Title' })
// Act
render(<Header {...props} />)
// 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)
})
})
})
})