refactor(web): migrate to Vitest and esm (#29974)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
Stephen Zhou
2025-12-22 16:35:22 +08:00
committed by GitHub
parent 42f7ecda12
commit eabdc5f0eb
268 changed files with 5455 additions and 6307 deletions

View File

@@ -11,7 +11,7 @@ enum MockCredentialTypeEnum {
}
// Mock plugin-auth module to avoid deep import chain issues
jest.mock('@/app/components/plugins/plugin-auth', () => ({
vi.mock('@/app/components/plugins/plugin-auth', () => ({
CredentialTypeEnum: {
OAUTH2: 'oauth2',
API_KEY: 'api_key',
@@ -19,7 +19,7 @@ jest.mock('@/app/components/plugins/plugin-auth', () => ({
}))
// Mock portal-to-follow-elem - use React state to properly handle open/close
jest.mock('@/app/components/base/portal-to-follow-elem', () => {
vi.mock('@/app/components/base/portal-to-follow-elem', () => {
const MockPortalToFollowElem = ({ children, open }: any) => {
return (
<div data-testid="portal-root" data-open={open}>
@@ -85,14 +85,14 @@ const createMockCredentials = (count: number = 3): DataSourceCredential[] =>
const createDefaultProps = (overrides?: Partial<CredentialSelectorProps>): CredentialSelectorProps => ({
currentCredentialId: 'cred-1',
onCredentialChange: jest.fn(),
onCredentialChange: vi.fn(),
credentials: createMockCredentials(),
...overrides,
})
describe('CredentialSelector', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// ==========================================
@@ -277,7 +277,7 @@ describe('CredentialSelector', () => {
describe('onCredentialChange prop', () => {
it('should be called when selecting a credential', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<CredentialSelector {...props} />)
@@ -298,7 +298,7 @@ describe('CredentialSelector', () => {
['cred-3', 'Credential 3'],
])('should call onCredentialChange with %s when selecting %s', (credId, credentialName) => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<CredentialSelector {...props} />)
@@ -317,7 +317,7 @@ describe('CredentialSelector', () => {
it('should call onCredentialChange with cred-1 when selecting Credential 1 in dropdown', () => {
// Arrange - Start with cred-2 selected so cred-1 is only in dropdown
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({
onCredentialChange: mockOnChange,
currentCredentialId: 'cred-2',
@@ -359,7 +359,7 @@ describe('CredentialSelector', () => {
it('should call onCredentialChange when clicking a credential item', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<CredentialSelector {...props} />)
@@ -376,7 +376,7 @@ describe('CredentialSelector', () => {
it('should close dropdown after selecting a credential', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<CredentialSelector {...props} />)
@@ -410,7 +410,7 @@ describe('CredentialSelector', () => {
it('should allow selecting credentials multiple times', () => {
// Arrange - Start with cred-2 selected so we can select other credentials
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({
onCredentialChange: mockOnChange,
currentCredentialId: 'cred-2',
@@ -435,7 +435,7 @@ describe('CredentialSelector', () => {
describe('Side Effects and Cleanup', () => {
it('should auto-select first credential when currentCredential is not found and credentials exist', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({
currentCredentialId: 'non-existent-id',
onCredentialChange: mockOnChange,
@@ -450,7 +450,7 @@ describe('CredentialSelector', () => {
it('should not call onCredentialChange when currentCredential is found', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({
currentCredentialId: 'cred-2',
onCredentialChange: mockOnChange,
@@ -465,7 +465,7 @@ describe('CredentialSelector', () => {
it('should not call onCredentialChange when credentials array is empty', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({
currentCredentialId: 'cred-1',
credentials: [],
@@ -481,7 +481,7 @@ describe('CredentialSelector', () => {
it('should auto-select when credentials change and currentCredential becomes invalid', async () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const initialCredentials = createMockCredentials(3)
const props = createDefaultProps({
currentCredentialId: 'cred-1',
@@ -512,7 +512,7 @@ describe('CredentialSelector', () => {
it('should not trigger auto-select effect on every render with same props', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
// Act - Render and rerender with same props
@@ -531,7 +531,7 @@ describe('CredentialSelector', () => {
describe('Callback Stability and Memoization', () => {
it('should have stable handleCredentialChange callback', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<CredentialSelector {...props} />)
@@ -547,8 +547,8 @@ describe('CredentialSelector', () => {
it('should update handleCredentialChange when onCredentialChange changes', () => {
// Arrange
const mockOnChange1 = jest.fn()
const mockOnChange2 = jest.fn()
const mockOnChange1 = vi.fn()
const mockOnChange2 = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange1 })
const { rerender } = render(<CredentialSelector {...props} />)
@@ -618,7 +618,7 @@ describe('CredentialSelector', () => {
it('should return undefined currentCredential when id not found', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({
currentCredentialId: 'non-existent',
onCredentialChange: mockOnChange,
@@ -643,9 +643,9 @@ describe('CredentialSelector', () => {
it('should not re-render when props remain the same', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
const renderSpy = jest.fn()
const renderSpy = vi.fn()
const TrackedCredentialSelector: React.FC<CredentialSelectorProps> = (trackedProps) => {
renderSpy()
@@ -693,8 +693,8 @@ describe('CredentialSelector', () => {
it('should re-render when onCredentialChange reference changes', () => {
// Arrange
const mockOnChange1 = jest.fn()
const mockOnChange2 = jest.fn()
const mockOnChange1 = vi.fn()
const mockOnChange2 = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange1 })
const { rerender } = render(<CredentialSelector {...props} />)
@@ -845,7 +845,7 @@ describe('CredentialSelector', () => {
it('should handle credential selection with duplicate names', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const duplicateCredentials = [
createMockCredential({ id: 'cred-1', name: 'Same Name' }),
createMockCredential({ id: 'cred-2', name: 'Same Name' }),
@@ -875,7 +875,7 @@ describe('CredentialSelector', () => {
it('should not crash when clicking credential after unmount', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
const { unmount } = render(<CredentialSelector {...props} />)
@@ -1017,7 +1017,7 @@ describe('CredentialSelector', () => {
it('should pass handleCredentialChange to List component', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<CredentialSelector {...props} />)

View File

@@ -10,7 +10,7 @@ enum MockCredentialTypeEnum {
}
// Mock plugin-auth module to avoid deep import chain issues
jest.mock('@/app/components/plugins/plugin-auth', () => ({
vi.mock('@/app/components/plugins/plugin-auth', () => ({
CredentialTypeEnum: {
OAUTH2: 'oauth2',
API_KEY: 'api_key',
@@ -18,7 +18,7 @@ jest.mock('@/app/components/plugins/plugin-auth', () => ({
}))
// Mock portal-to-follow-elem - required for CredentialSelector
jest.mock('@/app/components/base/portal-to-follow-elem', () => {
vi.mock('@/app/components/base/portal-to-follow-elem', () => {
const MockPortalToFollowElem = ({ children, open }: any) => {
return (
<div data-testid="portal-root" data-open={open}>
@@ -84,14 +84,14 @@ const createDefaultProps = (overrides?: Partial<HeaderProps>): HeaderProps => ({
docLink: 'https://docs.example.com',
pluginName: 'Test Plugin',
currentCredentialId: 'cred-1',
onCredentialChange: jest.fn(),
onCredentialChange: vi.fn(),
credentials: createMockCredentials(),
...overrides,
})
describe('Header', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// ==========================================
@@ -266,7 +266,7 @@ describe('Header', () => {
describe('onClickConfiguration prop', () => {
it('should call onClickConfiguration when configuration icon is clicked', () => {
// Arrange
const mockOnClick = jest.fn()
const mockOnClick = vi.fn()
const props = createDefaultProps({ onClickConfiguration: mockOnClick })
render(<Header {...props} />)
@@ -328,7 +328,7 @@ describe('Header', () => {
it('should pass onCredentialChange to CredentialSelector', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<Header {...props} />)
@@ -363,7 +363,7 @@ describe('Header', () => {
it('should allow credential selection through CredentialSelector', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnChange })
render(<Header {...props} />)
@@ -377,7 +377,7 @@ describe('Header', () => {
it('should trigger configuration callback when clicking config icon', () => {
// Arrange
const mockOnConfig = jest.fn()
const mockOnConfig = vi.fn()
const props = createDefaultProps({ onClickConfiguration: mockOnConfig })
const { container } = render(<Header {...props} />)
@@ -402,7 +402,7 @@ describe('Header', () => {
it('should not re-render when props remain the same', () => {
// Arrange
const props = createDefaultProps()
const renderSpy = jest.fn()
const renderSpy = vi.fn()
const TrackedHeader: React.FC<HeaderProps> = (trackedProps) => {
renderSpy()
@@ -573,7 +573,7 @@ describe('Header', () => {
describe('Integration', () => {
it('should work with full credential workflow', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({
onCredentialChange: mockOnCredentialChange,
currentCredentialId: 'cred-1',
@@ -597,7 +597,7 @@ describe('Header', () => {
it('should display all components together correctly', () => {
// Arrange
const mockOnConfig = jest.fn()
const mockOnConfig = vi.fn()
const props = createDefaultProps({
docTitle: 'Integration Test Docs',
docLink: 'https://test.com/docs',

View File

@@ -9,45 +9,54 @@ import { VarKindType } from '@/app/components/workflow/nodes/_base/types'
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock useDocLink - context hook requires mocking
const mockDocLink = jest.fn((path?: string) => `https://docs.example.com${path || ''}`)
jest.mock('@/context/i18n', () => ({
const mockDocLink = vi.fn((path?: string) => `https://docs.example.com${path || ''}`)
vi.mock('@/context/i18n', () => ({
useDocLink: () => mockDocLink,
}))
// Mock dataset-detail context - context provider requires mocking
let mockPipelineId = 'pipeline-123'
jest.mock('@/context/dataset-detail', () => ({
vi.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', () => ({
const mockSetShowAccountSettingModal = vi.fn()
vi.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),
const { mockSsePost } = vi.hoisted(() => ({
mockSsePost: vi.fn(),
}))
vi.mock('@/service/base', () => ({
ssePost: mockSsePost,
}))
// Mock Toast.notify - static method that manipulates DOM, needs mocking to verify calls
const mockToastNotify = jest.fn()
jest.mock('@/app/components/base/toast', () => ({
const { mockToastNotify } = vi.hoisted(() => ({
mockToastNotify: vi.fn(),
}))
vi.mock('@/app/components/base/toast', () => ({
__esModule: true,
default: {
notify: (options: any) => mockToastNotify(options),
notify: mockToastNotify,
},
}))
// Mock useGetDataSourceAuth - API service hook requires mocking
const mockUseGetDataSourceAuth = jest.fn()
jest.mock('@/service/use-datasource', () => ({
useGetDataSourceAuth: (params: any) => mockUseGetDataSourceAuth(params),
const { mockUseGetDataSourceAuth } = vi.hoisted(() => ({
mockUseGetDataSourceAuth: vi.fn(),
}))
vi.mock('@/service/use-datasource', () => ({
useGetDataSourceAuth: mockUseGetDataSourceAuth,
}))
// Note: zustand/react/shallow useShallow is imported directly (simple utility function)
@@ -58,24 +67,24 @@ const mockStoreState = {
searchValue: '',
selectedPagesId: new Set<string>(),
currentCredentialId: '',
setDocumentsData: jest.fn(),
setSearchValue: jest.fn(),
setSelectedPagesId: jest.fn(),
setOnlineDocuments: jest.fn(),
setCurrentDocument: jest.fn(),
setDocumentsData: vi.fn(),
setSearchValue: vi.fn(),
setSelectedPagesId: vi.fn(),
setOnlineDocuments: vi.fn(),
setCurrentDocument: vi.fn(),
}
const mockGetState = jest.fn(() => mockStoreState)
const mockGetState = vi.fn(() => mockStoreState)
const mockDataSourceStore = { getState: mockGetState }
jest.mock('../store', () => ({
vi.mock('../store', () => ({
useDataSourceStoreWithSelector: (selector: (s: any) => any) => selector(mockStoreState),
useDataSourceStore: () => mockDataSourceStore,
}))
// Mock Header component
jest.mock('../base/header', () => {
const MockHeader = (props: any) => (
vi.mock('../base/header', () => ({
default: (props: any) => (
<div data-testid="header">
<span data-testid="header-doc-title">{props.docTitle}</span>
<span data-testid="header-doc-link">{props.docLink}</span>
@@ -85,13 +94,12 @@ jest.mock('../base/header', () => {
<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 SearchInput component
jest.mock('@/app/components/base/notion-page-selector/search-input', () => {
const MockSearchInput = ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
vi.mock('@/app/components/base/notion-page-selector/search-input', () => ({
default: ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
<div data-testid="search-input">
<input
data-testid="search-input-field"
@@ -100,13 +108,12 @@ jest.mock('@/app/components/base/notion-page-selector/search-input', () => {
placeholder="Search"
/>
</div>
)
return MockSearchInput
})
),
}))
// Mock PageSelector component
jest.mock('./page-selector', () => {
const MockPageSelector = (props: any) => (
vi.mock('./page-selector', () => ({
default: (props: any) => (
<div data-testid="page-selector">
<span data-testid="page-selector-checked-count">{props.checkedIds?.size || 0}</span>
<span data-testid="page-selector-search-value">{props.searchValue}</span>
@@ -126,27 +133,17 @@ jest.mock('./page-selector', () => {
Preview Page
</button>
</div>
)
return MockPageSelector
})
),
}))
// Mock Title component
jest.mock('./title', () => {
const MockTitle = ({ name }: { name: string }) => (
vi.mock('./title', () => ({
default: ({ name }: { name: string }) => (
<div data-testid="title">
<span data-testid="title-name">{name}</span>
</div>
)
return MockTitle
})
// Mock Loading component
jest.mock('@/app/components/base/loading', () => {
const MockLoading = ({ type }: { type: string }) => (
<div data-testid="loading" data-type={type}>Loading...</div>
)
return MockLoading
})
),
}))
// ==========================================
// Test Data Builders
@@ -197,7 +194,7 @@ type OnlineDocumentsProps = React.ComponentProps<typeof OnlineDocuments>
const createDefaultProps = (overrides?: Partial<OnlineDocumentsProps>): OnlineDocumentsProps => ({
nodeId: 'node-1',
nodeData: createMockNodeData(),
onCredentialChange: jest.fn(),
onCredentialChange: vi.fn(),
isInPipeline: false,
supportBatchUpload: true,
...overrides,
@@ -208,18 +205,18 @@ const createDefaultProps = (overrides?: Partial<OnlineDocumentsProps>): OnlineDo
// ==========================================
describe('OnlineDocuments', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Reset store state
mockStoreState.documentsData = []
mockStoreState.searchValue = ''
mockStoreState.selectedPagesId = new Set()
mockStoreState.currentCredentialId = ''
mockStoreState.setDocumentsData = jest.fn()
mockStoreState.setSearchValue = jest.fn()
mockStoreState.setSelectedPagesId = jest.fn()
mockStoreState.setOnlineDocuments = jest.fn()
mockStoreState.setCurrentDocument = jest.fn()
mockStoreState.setDocumentsData = vi.fn()
mockStoreState.setSearchValue = vi.fn()
mockStoreState.setSelectedPagesId = vi.fn()
mockStoreState.setOnlineDocuments = vi.fn()
mockStoreState.setCurrentDocument = vi.fn()
// Reset context values
mockPipelineId = 'pipeline-123'
@@ -273,8 +270,7 @@ describe('OnlineDocuments', () => {
render(<OnlineDocuments {...props} />)
// Assert
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByTestId('loading')).toHaveAttribute('data-type', 'app')
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should render PageSelector when documentsData has content', () => {
@@ -287,7 +283,7 @@ describe('OnlineDocuments', () => {
// Assert
expect(screen.getByTestId('page-selector')).toBeInTheDocument()
expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
it('should render Title with datasource_label', () => {
@@ -493,7 +489,7 @@ describe('OnlineDocuments', () => {
describe('onCredentialChange prop', () => {
it('should pass onCredentialChange to Header', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
// Act
@@ -761,7 +757,7 @@ describe('OnlineDocuments', () => {
render(<OnlineDocuments {...props} />)
// Assert - Should show loading instead of PageSelector
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
})
})
@@ -831,7 +827,7 @@ describe('OnlineDocuments', () => {
it('should handle credential change', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
render(<OnlineDocuments {...props} />)
@@ -1032,7 +1028,7 @@ describe('OnlineDocuments', () => {
render(<OnlineDocuments {...props} />)
// Assert - Should show loading when documentsData is undefined
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should handle undefined datasource_parameters (line 79 branch)', () => {
@@ -1219,7 +1215,7 @@ describe('OnlineDocuments', () => {
const props: OnlineDocumentsProps = {
nodeId: 'node-1',
nodeData: createMockNodeData(),
onCredentialChange: jest.fn(),
onCredentialChange: vi.fn(),
// isInPipeline and supportBatchUpload are not provided
}
@@ -1303,13 +1299,13 @@ describe('OnlineDocuments', () => {
})
// Should still show loading since documentsData is empty
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should handle credential change and refetch documents', () => {
// Arrange
mockStoreState.currentCredentialId = 'initial-cred'
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
// Act
@@ -1325,33 +1321,4 @@ describe('OnlineDocuments', () => {
})
// ==========================================
// Styling
// ==========================================
describe('Styling', () => {
it('should apply correct container classes', () => {
// Arrange
const props = createDefaultProps()
// Act
const { container } = render(<OnlineDocuments {...props} />)
// Assert
const rootDiv = container.firstChild as HTMLElement
expect(rootDiv).toHaveClass('flex', 'flex-col', 'gap-y-2')
})
it('should apply correct classes to main content container', () => {
// Arrange
mockStoreState.documentsData = [createMockWorkspace()]
const props = createDefaultProps()
// Act
const { container } = render(<OnlineDocuments {...props} />)
// Assert
const contentContainer = container.querySelector('.rounded-xl.border')
expect(contentContainer).toBeInTheDocument()
expect(contentContainer).toHaveClass('border-components-panel-border', 'bg-background-default-subtle')
})
})
})

View File

@@ -9,10 +9,10 @@ import { recursivePushInParentDescendants } from './utils'
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock react-window FixedSizeList - renders items directly for testing
jest.mock('react-window', () => ({
vi.mock('react-window', () => ({
FixedSizeList: ({ children: ItemComponent, itemCount, itemData, itemKey }: any) => (
<div data-testid="virtual-list">
{Array.from({ length: itemCount }).map((_, index) => (
@@ -25,6 +25,7 @@ jest.mock('react-window', () => ({
))}
</div>
),
areEqual: (prevProps: any, nextProps: any) => prevProps === nextProps,
}))
// Note: NotionIcon from @/app/components/base/ is NOT mocked - using real component per testing guidelines
@@ -76,9 +77,9 @@ const createDefaultProps = (overrides?: Partial<PageSelectorProps>): PageSelecto
searchValue: '',
pagesMap: createMockPagesMap(defaultList),
list: defaultList,
onSelect: jest.fn(),
onSelect: vi.fn(),
canPreview: true,
onPreview: jest.fn(),
onPreview: vi.fn(),
isMultipleChoice: true,
currentCredentialId: 'cred-1',
...overrides,
@@ -103,7 +104,7 @@ const createHierarchicalPages = () => {
// ==========================================
describe('PageSelector', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// ==========================================
@@ -539,7 +540,7 @@ describe('PageSelector', () => {
describe('onSelect prop', () => {
it('should call onSelect when checkbox is clicked', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const props = createDefaultProps({ onSelect: mockOnSelect })
// Act
@@ -553,7 +554,7 @@ describe('PageSelector', () => {
it('should pass updated set to onSelect', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const page = createMockPage({ page_id: 'page-1' })
const props = createDefaultProps({
list: [page],
@@ -575,7 +576,7 @@ describe('PageSelector', () => {
describe('onPreview prop', () => {
it('should call onPreview when preview button is clicked', () => {
// Arrange
const mockOnPreview = jest.fn()
const mockOnPreview = vi.fn()
const page = createMockPage({ page_id: 'page-1' })
const props = createDefaultProps({
list: [page],
@@ -679,7 +680,7 @@ describe('PageSelector', () => {
it('should maintain currentPreviewPageId state', () => {
// Arrange
const mockOnPreview = jest.fn()
const mockOnPreview = vi.fn()
const pages = [
createMockPage({ page_id: 'page-1', page_name: 'Page 1' }),
createMockPage({ page_id: 'page-2', page_name: 'Page 2' }),
@@ -833,7 +834,7 @@ describe('PageSelector', () => {
it('should have stable handleCheck that adds page and descendants to selection', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const { list, pagesMap } = createHierarchicalPages()
const props = createDefaultProps({
list,
@@ -857,7 +858,7 @@ describe('PageSelector', () => {
it('should have stable handleCheck that removes page and descendants from selection', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const { list, pagesMap } = createHierarchicalPages()
const props = createDefaultProps({
list,
@@ -879,7 +880,7 @@ describe('PageSelector', () => {
it('should have stable handlePreview that updates currentPreviewPageId', () => {
// Arrange
const mockOnPreview = jest.fn()
const mockOnPreview = vi.fn()
const page = createMockPage({ page_id: 'preview-page' })
const props = createDefaultProps({
list: [page],
@@ -1007,7 +1008,7 @@ describe('PageSelector', () => {
it('should check/uncheck page when clicking checkbox', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const props = createDefaultProps({
onSelect: mockOnSelect,
checkedIds: new Set(),
@@ -1023,7 +1024,7 @@ describe('PageSelector', () => {
it('should select radio when clicking in single choice mode', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const props = createDefaultProps({
onSelect: mockOnSelect,
isMultipleChoice: false,
@@ -1040,7 +1041,7 @@ describe('PageSelector', () => {
it('should clear previous selection in single choice mode', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const pages = [
createMockPage({ page_id: 'page-1', page_name: 'Page 1' }),
createMockPage({ page_id: 'page-2', page_name: 'Page 2' }),
@@ -1067,7 +1068,7 @@ describe('PageSelector', () => {
it('should trigger preview when clicking preview button', () => {
// Arrange
const mockOnPreview = jest.fn()
const mockOnPreview = vi.fn()
const props = createDefaultProps({
onPreview: mockOnPreview,
canPreview: true,
@@ -1083,7 +1084,7 @@ describe('PageSelector', () => {
it('should not cascade selection in search mode', () => {
// Arrange
const mockOnSelect = jest.fn()
const mockOnSelect = vi.fn()
const { list, pagesMap } = createHierarchicalPages()
const props = createDefaultProps({
list,
@@ -1359,7 +1360,7 @@ describe('PageSelector', () => {
searchValue: '',
pagesMap: createMockPagesMap([createMockPage()]),
list: [createMockPage()],
onSelect: jest.fn(),
onSelect: vi.fn(),
currentCredentialId: 'cred-1',
// canPreview defaults to true
// isMultipleChoice defaults to true

View File

@@ -6,11 +6,11 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-so
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock useToolIcon - hook has complex dependencies (API calls, stores)
const mockUseToolIcon = jest.fn()
jest.mock('@/app/components/workflow/hooks', () => ({
const mockUseToolIcon = vi.fn()
vi.mock('@/app/components/workflow/hooks', () => ({
useToolIcon: (data: any) => mockUseToolIcon(data),
}))
@@ -33,7 +33,7 @@ type ConnectProps = React.ComponentProps<typeof Connect>
const createDefaultProps = (overrides?: Partial<ConnectProps>): ConnectProps => ({
nodeData: createMockNodeData(),
onSetting: jest.fn(),
onSetting: vi.fn(),
...overrides,
})
@@ -42,7 +42,7 @@ const createDefaultProps = (overrides?: Partial<ConnectProps>): ConnectProps =>
// ==========================================
describe('Connect', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Default mock return values
mockUseToolIcon.mockReturnValue('https://example.com/icon.png')
@@ -216,7 +216,7 @@ describe('Connect', () => {
describe('onSetting prop', () => {
it('should call onSetting when connect button is clicked', () => {
// Arrange
const mockOnSetting = jest.fn()
const mockOnSetting = vi.fn()
const props = createDefaultProps({ onSetting: mockOnSetting })
// Act
@@ -229,7 +229,7 @@ describe('Connect', () => {
it('should call onSetting when button clicked', () => {
// Arrange
const mockOnSetting = jest.fn()
const mockOnSetting = vi.fn()
const props = createDefaultProps({ onSetting: mockOnSetting })
// Act
@@ -243,7 +243,7 @@ describe('Connect', () => {
it('should call onSetting on each button click', () => {
// Arrange
const mockOnSetting = jest.fn()
const mockOnSetting = vi.fn()
const props = createDefaultProps({ onSetting: mockOnSetting })
// Act
@@ -266,7 +266,7 @@ describe('Connect', () => {
describe('Connect Button', () => {
it('should trigger onSetting callback on click', () => {
// Arrange
const mockOnSetting = jest.fn()
const mockOnSetting = vi.fn()
const props = createDefaultProps({ onSetting: mockOnSetting })
render(<Connect {...props} />)
@@ -291,7 +291,7 @@ describe('Connect', () => {
it('should handle keyboard interaction (Enter key)', () => {
// Arrange
const mockOnSetting = jest.fn()
const mockOnSetting = vi.fn()
const props = createDefaultProps({ onSetting: mockOnSetting })
render(<Connect {...props} />)

View File

@@ -3,7 +3,7 @@ import React from 'react'
import Dropdown from './index'
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// ==========================================
// ==========================================
@@ -14,7 +14,7 @@ type DropdownProps = React.ComponentProps<typeof Dropdown>
const createDefaultProps = (overrides?: Partial<DropdownProps>): DropdownProps => ({
startIndex: 0,
breadcrumbs: ['folder1', 'folder2'],
onBreadcrumbClick: jest.fn(),
onBreadcrumbClick: vi.fn(),
...overrides,
})
@@ -23,7 +23,7 @@ const createDefaultProps = (overrides?: Partial<DropdownProps>): DropdownProps =
// ==========================================
describe('Dropdown', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
// ==========================================
@@ -115,7 +115,7 @@ describe('Dropdown', () => {
describe('startIndex prop', () => {
it('should pass startIndex to Menu component', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 5,
breadcrumbs: ['folder1'],
@@ -138,7 +138,7 @@ describe('Dropdown', () => {
it('should calculate correct index for second item', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 3,
breadcrumbs: ['folder1', 'folder2'],
@@ -252,7 +252,7 @@ describe('Dropdown', () => {
describe('onBreadcrumbClick prop', () => {
it('should call onBreadcrumbClick with correct index when item clicked', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 0,
breadcrumbs: ['folder1'],
@@ -327,7 +327,7 @@ describe('Dropdown', () => {
it('should close when breadcrumb item is clicked', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
breadcrumbs: ['test-folder'],
onBreadcrumbClick: mockOnBreadcrumbClick,
@@ -422,7 +422,7 @@ describe('Dropdown', () => {
describe('handleBreadCrumbClick', () => {
it('should call onBreadcrumbClick and close menu', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
breadcrumbs: ['folder1'],
onBreadcrumbClick: mockOnBreadcrumbClick,
@@ -450,7 +450,7 @@ describe('Dropdown', () => {
it('should pass correct index to onBreadcrumbClick for each item', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 2,
breadcrumbs: ['folder1', 'folder2', 'folder3'],
@@ -484,7 +484,7 @@ describe('Dropdown', () => {
it('should maintain stable callback after rerender with same props', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
breadcrumbs: ['folder'],
onBreadcrumbClick: mockOnBreadcrumbClick,
@@ -512,8 +512,8 @@ describe('Dropdown', () => {
it('should update callback when onBreadcrumbClick prop changes', async () => {
// Arrange
const mockOnBreadcrumbClick1 = jest.fn()
const mockOnBreadcrumbClick2 = jest.fn()
const mockOnBreadcrumbClick1 = vi.fn()
const mockOnBreadcrumbClick2 = vi.fn()
const props = createDefaultProps({
breadcrumbs: ['folder'],
onBreadcrumbClick: mockOnBreadcrumbClick1,
@@ -616,7 +616,7 @@ describe('Dropdown', () => {
it('should handle startIndex of 0', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 0,
breadcrumbs: ['folder'],
@@ -637,7 +637,7 @@ describe('Dropdown', () => {
it('should handle large startIndex values', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 999,
breadcrumbs: ['folder'],
@@ -700,7 +700,7 @@ describe('Dropdown', () => {
{ startIndex: 10, breadcrumbs: ['a', 'b'], expectedIndex: 10 },
])('should handle startIndex=$startIndex correctly', async ({ startIndex, breadcrumbs, expectedIndex }) => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex,
breadcrumbs,
@@ -764,7 +764,7 @@ describe('Dropdown', () => {
it('should handle click on any menu item', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 0,
breadcrumbs: ['first', 'second', 'third'],
@@ -785,7 +785,7 @@ describe('Dropdown', () => {
it('should close menu after any item click', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
breadcrumbs: ['item1', 'item2', 'item3'],
onBreadcrumbClick: mockOnBreadcrumbClick,
@@ -809,7 +809,7 @@ describe('Dropdown', () => {
it('should correctly calculate index for each item based on startIndex', async () => {
// Arrange
const mockOnBreadcrumbClick = jest.fn()
const mockOnBreadcrumbClick = vi.fn()
const props = createDefaultProps({
startIndex: 3,
breadcrumbs: ['folder-a', 'folder-b', 'folder-c'],

View File

@@ -6,24 +6,24 @@ import Breadcrumbs from './index'
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock store - context provider requires mocking
const mockStoreState = {
hasBucket: false,
breadcrumbs: [] as string[],
prefix: [] as string[],
setOnlineDriveFileList: jest.fn(),
setSelectedFileIds: jest.fn(),
setBreadcrumbs: jest.fn(),
setPrefix: jest.fn(),
setBucket: jest.fn(),
setOnlineDriveFileList: vi.fn(),
setSelectedFileIds: vi.fn(),
setBreadcrumbs: vi.fn(),
setPrefix: vi.fn(),
setBucket: vi.fn(),
}
const mockGetState = jest.fn(() => mockStoreState)
const mockGetState = vi.fn(() => mockStoreState)
const mockDataSourceStore = { getState: mockGetState }
jest.mock('../../../../store', () => ({
vi.mock('../../../../store', () => ({
useDataSourceStore: () => mockDataSourceStore,
useDataSourceStoreWithSelector: (selector: (s: typeof mockStoreState) => unknown) => selector(mockStoreState),
}))
@@ -49,11 +49,11 @@ const resetMockStoreState = () => {
mockStoreState.hasBucket = false
mockStoreState.breadcrumbs = []
mockStoreState.prefix = []
mockStoreState.setOnlineDriveFileList = jest.fn()
mockStoreState.setSelectedFileIds = jest.fn()
mockStoreState.setBreadcrumbs = jest.fn()
mockStoreState.setPrefix = jest.fn()
mockStoreState.setBucket = jest.fn()
mockStoreState.setOnlineDriveFileList = vi.fn()
mockStoreState.setSelectedFileIds = vi.fn()
mockStoreState.setBreadcrumbs = vi.fn()
mockStoreState.setPrefix = vi.fn()
mockStoreState.setBucket = vi.fn()
}
// ==========================================
@@ -61,7 +61,7 @@ const resetMockStoreState = () => {
// ==========================================
describe('Breadcrumbs', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
resetMockStoreState()
})

View File

@@ -6,24 +6,24 @@ import Header from './index'
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock store - required by Breadcrumbs component
const mockStoreState = {
hasBucket: false,
setOnlineDriveFileList: jest.fn(),
setSelectedFileIds: jest.fn(),
setBreadcrumbs: jest.fn(),
setPrefix: jest.fn(),
setBucket: jest.fn(),
setOnlineDriveFileList: vi.fn(),
setSelectedFileIds: vi.fn(),
setBreadcrumbs: vi.fn(),
setPrefix: vi.fn(),
setBucket: vi.fn(),
breadcrumbs: [],
prefix: [],
}
const mockGetState = jest.fn(() => mockStoreState)
const mockGetState = vi.fn(() => mockStoreState)
const mockDataSourceStore = { getState: mockGetState }
jest.mock('../../../store', () => ({
vi.mock('../../../store', () => ({
useDataSourceStore: () => mockDataSourceStore,
useDataSourceStoreWithSelector: (selector: (s: typeof mockStoreState) => unknown) => selector(mockStoreState),
}))
@@ -39,8 +39,8 @@ const createDefaultProps = (overrides?: Partial<HeaderProps>): HeaderProps => ({
keywords: '',
bucket: '',
searchResultsLength: 0,
handleInputChange: jest.fn(),
handleResetKeywords: jest.fn(),
handleInputChange: vi.fn(),
handleResetKeywords: vi.fn(),
isInPipeline: false,
...overrides,
})
@@ -50,11 +50,11 @@ const createDefaultProps = (overrides?: Partial<HeaderProps>): HeaderProps => ({
// ==========================================
const resetMockStoreState = () => {
mockStoreState.hasBucket = false
mockStoreState.setOnlineDriveFileList = jest.fn()
mockStoreState.setSelectedFileIds = jest.fn()
mockStoreState.setBreadcrumbs = jest.fn()
mockStoreState.setPrefix = jest.fn()
mockStoreState.setBucket = jest.fn()
mockStoreState.setOnlineDriveFileList = vi.fn()
mockStoreState.setSelectedFileIds = vi.fn()
mockStoreState.setBreadcrumbs = vi.fn()
mockStoreState.setPrefix = vi.fn()
mockStoreState.setBucket = vi.fn()
mockStoreState.breadcrumbs = []
mockStoreState.prefix = []
}
@@ -64,7 +64,7 @@ const resetMockStoreState = () => {
// ==========================================
describe('Header', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
resetMockStoreState()
})
@@ -333,7 +333,7 @@ describe('Header', () => {
describe('handleInputChange', () => {
it('should call handleInputChange when input value changes', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleInputChange = vi.fn()
const props = createDefaultProps({ handleInputChange: mockHandleInputChange })
render(<Header {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -349,7 +349,7 @@ describe('Header', () => {
it('should call handleInputChange on each keystroke', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleInputChange = vi.fn()
const props = createDefaultProps({ handleInputChange: mockHandleInputChange })
render(<Header {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -365,7 +365,7 @@ describe('Header', () => {
it('should handle empty string input', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleInputChange = vi.fn()
const props = createDefaultProps({ inputValue: 'existing', handleInputChange: mockHandleInputChange })
render(<Header {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -380,7 +380,7 @@ describe('Header', () => {
it('should handle whitespace-only input', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleInputChange = vi.fn()
const props = createDefaultProps({ handleInputChange: mockHandleInputChange })
render(<Header {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -397,7 +397,7 @@ describe('Header', () => {
describe('handleResetKeywords', () => {
it('should call handleResetKeywords when clear icon is clicked', () => {
// Arrange
const mockHandleResetKeywords = jest.fn()
const mockHandleResetKeywords = vi.fn()
const props = createDefaultProps({
inputValue: 'to-clear',
handleResetKeywords: mockHandleResetKeywords,
@@ -446,8 +446,8 @@ describe('Header', () => {
it('should not re-render when props are the same', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleResetKeywords = jest.fn()
const mockHandleInputChange = vi.fn()
const mockHandleResetKeywords = vi.fn()
const props = createDefaultProps({
handleInputChange: mockHandleInputChange,
handleResetKeywords: mockHandleResetKeywords,
@@ -571,7 +571,7 @@ describe('Header', () => {
it('should pass the event object to handleInputChange callback', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleInputChange = vi.fn()
const props = createDefaultProps({ handleInputChange: mockHandleInputChange })
render(<Header {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -664,8 +664,8 @@ describe('Header', () => {
it('should pass correct props to Input component', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleResetKeywords = jest.fn()
const mockHandleInputChange = vi.fn()
const mockHandleResetKeywords = vi.fn()
const props = createDefaultProps({
inputValue: 'test-input',
handleInputChange: mockHandleInputChange,
@@ -691,7 +691,7 @@ describe('Header', () => {
describe('Callback Stability', () => {
it('should maintain stable handleInputChange callback after rerender', () => {
// Arrange
const mockHandleInputChange = jest.fn()
const mockHandleInputChange = vi.fn()
const props = createDefaultProps({ handleInputChange: mockHandleInputChange })
const { rerender } = render(<Header {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -707,7 +707,7 @@ describe('Header', () => {
it('should maintain stable handleResetKeywords callback after rerender', () => {
// Arrange
const mockHandleResetKeywords = jest.fn()
const mockHandleResetKeywords = vi.fn()
const props = createDefaultProps({
inputValue: 'to-clear',
handleResetKeywords: mockHandleResetKeywords,

View File

@@ -8,11 +8,11 @@ import { OnlineDriveFileType } from '@/models/pipeline'
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock ahooks useDebounceFn - third-party library requires mocking
const mockDebounceFnRun = jest.fn()
jest.mock('ahooks', () => ({
const mockDebounceFnRun = vi.fn()
vi.mock('ahooks', () => ({
useDebounceFn: (fn: (...args: any[]) => void) => {
mockDebounceFnRun.mockImplementation(fn)
return { run: mockDebounceFnRun }
@@ -21,21 +21,21 @@ jest.mock('ahooks', () => ({
// Mock store - context provider requires mocking
const mockStoreState = {
setNextPageParameters: jest.fn(),
setNextPageParameters: vi.fn(),
currentNextPageParametersRef: { current: {} },
isTruncated: { current: false },
hasBucket: false,
setOnlineDriveFileList: jest.fn(),
setSelectedFileIds: jest.fn(),
setBreadcrumbs: jest.fn(),
setPrefix: jest.fn(),
setBucket: jest.fn(),
setOnlineDriveFileList: vi.fn(),
setSelectedFileIds: vi.fn(),
setBreadcrumbs: vi.fn(),
setPrefix: vi.fn(),
setBucket: vi.fn(),
}
const mockGetState = jest.fn(() => mockStoreState)
const mockGetState = vi.fn(() => mockStoreState)
const mockDataSourceStore = { getState: mockGetState }
jest.mock('../../store', () => ({
vi.mock('../../store', () => ({
useDataSourceStore: () => mockDataSourceStore,
useDataSourceStoreWithSelector: (selector: (s: any) => any) => selector(mockStoreState),
}))
@@ -60,11 +60,11 @@ const createDefaultProps = (overrides?: Partial<FileListProps>): FileListProps =
keywords: '',
bucket: '',
isInPipeline: false,
resetKeywords: jest.fn(),
updateKeywords: jest.fn(),
resetKeywords: vi.fn(),
updateKeywords: vi.fn(),
searchResultsLength: 0,
handleSelectFile: jest.fn(),
handleOpenFolder: jest.fn(),
handleSelectFile: vi.fn(),
handleOpenFolder: vi.fn(),
isLoading: false,
supportBatchUpload: true,
...overrides,
@@ -74,15 +74,15 @@ const createDefaultProps = (overrides?: Partial<FileListProps>): FileListProps =
// Helper Functions
// ==========================================
const resetMockStoreState = () => {
mockStoreState.setNextPageParameters = jest.fn()
mockStoreState.setNextPageParameters = vi.fn()
mockStoreState.currentNextPageParametersRef = { current: {} }
mockStoreState.isTruncated = { current: false }
mockStoreState.hasBucket = false
mockStoreState.setOnlineDriveFileList = jest.fn()
mockStoreState.setSelectedFileIds = jest.fn()
mockStoreState.setBreadcrumbs = jest.fn()
mockStoreState.setPrefix = jest.fn()
mockStoreState.setBucket = jest.fn()
mockStoreState.setOnlineDriveFileList = vi.fn()
mockStoreState.setSelectedFileIds = vi.fn()
mockStoreState.setBreadcrumbs = vi.fn()
mockStoreState.setPrefix = vi.fn()
mockStoreState.setBucket = vi.fn()
}
// ==========================================
@@ -90,7 +90,7 @@ const resetMockStoreState = () => {
// ==========================================
describe('FileList', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
resetMockStoreState()
mockDebounceFnRun.mockClear()
})
@@ -345,7 +345,7 @@ describe('FileList', () => {
describe('debounced keywords update', () => {
it('should call updateKeywords with debounce when input changes', () => {
// Arrange
const mockUpdateKeywords = jest.fn()
const mockUpdateKeywords = vi.fn()
const props = createDefaultProps({ updateKeywords: mockUpdateKeywords })
render(<FileList {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -379,7 +379,7 @@ describe('FileList', () => {
it('should trigger debounced updateKeywords on input change', () => {
// Arrange
const mockUpdateKeywords = jest.fn()
const mockUpdateKeywords = vi.fn()
const props = createDefaultProps({ updateKeywords: mockUpdateKeywords })
render(<FileList {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -393,7 +393,7 @@ describe('FileList', () => {
it('should handle multiple sequential input changes', () => {
// Arrange
const mockUpdateKeywords = jest.fn()
const mockUpdateKeywords = vi.fn()
const props = createDefaultProps({ updateKeywords: mockUpdateKeywords })
render(<FileList {...props} />)
const input = screen.getByPlaceholderText('datasetPipeline.onlineDrive.breadcrumbs.searchPlaceholder')
@@ -413,7 +413,7 @@ describe('FileList', () => {
describe('handleResetKeywords', () => {
it('should call resetKeywords prop when clear button is clicked', () => {
// Arrange
const mockResetKeywords = jest.fn()
const mockResetKeywords = vi.fn()
const props = createDefaultProps({ resetKeywords: mockResetKeywords, keywords: 'to-reset' })
const { container } = render(<FileList {...props} />)
@@ -446,7 +446,7 @@ describe('FileList', () => {
describe('handleSelectFile', () => {
it('should call handleSelectFile when file item is clicked', () => {
// Arrange
const mockHandleSelectFile = jest.fn()
const mockHandleSelectFile = vi.fn()
const fileList = [createMockOnlineDriveFile({ id: 'file-1', name: 'test.txt' })]
const props = createDefaultProps({ handleSelectFile: mockHandleSelectFile, fileList })
render(<FileList {...props} />)
@@ -467,7 +467,7 @@ describe('FileList', () => {
describe('handleOpenFolder', () => {
it('should call handleOpenFolder when folder item is clicked', () => {
// Arrange
const mockHandleOpenFolder = jest.fn()
const mockHandleOpenFolder = vi.fn()
const fileList = [createMockOnlineDriveFile({ id: 'folder-1', name: 'my-folder', type: OnlineDriveFileType.folder })]
const props = createDefaultProps({ handleOpenFolder: mockHandleOpenFolder, fileList })
render(<FileList {...props} />)
@@ -714,7 +714,7 @@ describe('FileList', () => {
describe('Callback Stability', () => {
it('should maintain stable handleSelectFile callback', () => {
// Arrange
const mockHandleSelectFile = jest.fn()
const mockHandleSelectFile = vi.fn()
const fileList = [createMockOnlineDriveFile({ id: 'file-1', name: 'test.txt' })]
const props = createDefaultProps({ handleSelectFile: mockHandleSelectFile, fileList })
const { rerender } = render(<FileList {...props} />)
@@ -735,7 +735,7 @@ describe('FileList', () => {
it('should maintain stable handleOpenFolder callback', () => {
// Arrange
const mockHandleOpenFolder = jest.fn()
const mockHandleOpenFolder = vi.fn()
const fileList = [createMockOnlineDriveFile({ id: 'folder-1', name: 'my-folder', type: OnlineDriveFileType.folder })]
const props = createDefaultProps({ handleOpenFolder: mockHandleOpenFolder, fileList })
const { rerender } = render(<FileList {...props} />)

View File

@@ -1,3 +1,4 @@
import type { Mock } from 'vitest'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import React from 'react'
import List from './index'
@@ -8,19 +9,11 @@ import { OnlineDriveFileType } from '@/models/pipeline'
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Mock Loading component - base component with simple render
jest.mock('@/app/components/base/loading', () => {
const MockLoading = ({ type }: { type?: string }) => (
<div data-testid="loading" data-type={type}>Loading...</div>
)
return MockLoading
})
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock Item component for List tests - child component with complex behavior
jest.mock('./item', () => {
const MockItem = ({ file, isSelected, onSelect, onOpen, isMultipleChoice }: {
vi.mock('./item', () => ({
default: ({ file, isSelected, onSelect, onOpen, isMultipleChoice }: {
file: OnlineDriveFile
isSelected: boolean
onSelect: (file: OnlineDriveFile) => void
@@ -38,33 +31,30 @@ jest.mock('./item', () => {
<button data-testid={`item-open-${file.id}`} onClick={() => onOpen(file)}>Open</button>
</div>
)
}
return MockItem
})
},
}))
// Mock EmptyFolder component for List tests
jest.mock('./empty-folder', () => {
const MockEmptyFolder = () => (
vi.mock('./empty-folder', () => ({
default: () => (
<div data-testid="empty-folder">Empty Folder</div>
)
return MockEmptyFolder
})
),
}))
// Mock EmptySearchResult component for List tests
jest.mock('./empty-search-result', () => {
const MockEmptySearchResult = ({ onResetKeywords }: { onResetKeywords: () => void }) => (
vi.mock('./empty-search-result', () => ({
default: ({ onResetKeywords }: { onResetKeywords: () => void }) => (
<div data-testid="empty-search-result">
<span>No results</span>
<button data-testid="reset-keywords-btn" onClick={onResetKeywords}>Reset</button>
</div>
)
return MockEmptySearchResult
})
),
}))
// Mock store state and refs
const mockIsTruncated = { current: false }
const mockCurrentNextPageParametersRef = { current: {} as Record<string, any> }
const mockSetNextPageParameters = jest.fn()
const mockSetNextPageParameters = vi.fn()
const mockStoreState = {
isTruncated: mockIsTruncated,
@@ -72,10 +62,10 @@ const mockStoreState = {
setNextPageParameters: mockSetNextPageParameters,
}
const mockGetState = jest.fn(() => mockStoreState)
const mockGetState = vi.fn(() => mockStoreState)
const mockDataSourceStore = { getState: mockGetState }
jest.mock('../../../store', () => ({
vi.mock('../../../store', () => ({
useDataSourceStore: () => mockDataSourceStore,
}))
@@ -106,9 +96,9 @@ const createDefaultProps = (overrides?: Partial<ListProps>): ListProps => ({
keywords: '',
isLoading: false,
supportBatchUpload: true,
handleResetKeywords: jest.fn(),
handleSelectFile: jest.fn(),
handleOpenFolder: jest.fn(),
handleResetKeywords: vi.fn(),
handleSelectFile: vi.fn(),
handleOpenFolder: vi.fn(),
...overrides,
})
@@ -117,16 +107,16 @@ const createDefaultProps = (overrides?: Partial<ListProps>): ListProps => ({
// ==========================================
let mockIntersectionObserverCallback: IntersectionObserverCallback | null = null
let mockIntersectionObserverInstance: {
observe: jest.Mock
disconnect: jest.Mock
unobserve: jest.Mock
observe: Mock
disconnect: Mock
unobserve: Mock
} | null = null
const createMockIntersectionObserver = () => {
const instance = {
observe: jest.fn(),
disconnect: jest.fn(),
unobserve: jest.fn(),
observe: vi.fn(),
disconnect: vi.fn(),
unobserve: vi.fn(),
}
mockIntersectionObserverInstance = instance
@@ -178,7 +168,7 @@ describe('List', () => {
const originalIntersectionObserver = window.IntersectionObserver
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
resetMockStoreState()
mockIntersectionObserverCallback = null
mockIntersectionObserverInstance = null
@@ -218,8 +208,7 @@ describe('List', () => {
render(<List {...props} />)
// Assert
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByTestId('loading')).toHaveAttribute('data-type', 'app')
expect(screen.getByRole('status')).toBeInTheDocument()
})
it('should render EmptyFolder when folder is empty and not loading', () => {
@@ -274,40 +263,12 @@ describe('List', () => {
isLoading: true,
})
// Act
const { container } = render(<List {...props} />)
// Assert - Should show files AND loading spinner (animation-spin class)
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
expect(container.querySelector('.animation-spin')).toBeInTheDocument()
})
it('should not render Loading component when partial loading', () => {
// Arrange
const fileList = createMockFileList(2)
const props = createDefaultProps({
fileList,
isLoading: true,
})
// Act
render(<List {...props} />)
// Assert - Full page loading should not appear
expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
})
it('should render anchor div for infinite scroll', () => {
// Arrange
const fileList = createMockFileList(2)
const props = createDefaultProps({ fileList })
// Act
const { container } = render(<List {...props} />)
// Assert - Anchor div should exist with h-0 class
const anchorDiv = container.querySelector('.h-0')
expect(anchorDiv).toBeInTheDocument()
// Assert - Should show files AND loading indicator
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
})
})
@@ -462,15 +423,16 @@ describe('List', () => {
const props = createDefaultProps({ isLoading, fileList })
// Act
const { container } = render(<List {...props} />)
render(<List {...props} />)
// Assert
switch (expected) {
case 'isAllLoading':
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
break
case 'isPartialLoading':
expect(container.querySelector('.animation-spin')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
break
case 'isEmpty':
expect(screen.getByTestId('empty-folder')).toBeInTheDocument()
@@ -522,7 +484,7 @@ describe('List', () => {
describe('File Selection', () => {
it('should call handleSelectFile when selecting a file', () => {
// Arrange
const handleSelectFile = jest.fn()
const handleSelectFile = vi.fn()
const fileList = createMockFileList(2)
const props = createDefaultProps({
fileList,
@@ -539,7 +501,7 @@ describe('List', () => {
it('should call handleSelectFile with correct file data', () => {
// Arrange
const handleSelectFile = jest.fn()
const handleSelectFile = vi.fn()
const fileList = [
createMockOnlineDriveFile({ id: 'unique-id', name: 'special-file.pdf', size: 5000 }),
]
@@ -566,7 +528,7 @@ describe('List', () => {
describe('Folder Navigation', () => {
it('should call handleOpenFolder when opening a folder', () => {
// Arrange
const handleOpenFolder = jest.fn()
const handleOpenFolder = vi.fn()
const fileList = [
createMockOnlineDriveFile({ id: 'folder-1', name: 'Documents', type: OnlineDriveFileType.folder }),
]
@@ -587,7 +549,7 @@ describe('List', () => {
describe('Reset Keywords', () => {
it('should call handleResetKeywords when reset button is clicked', () => {
// Arrange
const handleResetKeywords = jest.fn()
const handleResetKeywords = vi.fn()
const props = createDefaultProps({
fileList: [],
keywords: 'search-term',
@@ -639,12 +601,13 @@ describe('List', () => {
const props = createDefaultProps({ fileList })
// Act
const { container } = render(<List {...props} />)
render(<List {...props} />)
// Assert
expect(mockIntersectionObserverInstance?.observe).toHaveBeenCalled()
const anchorDiv = container.querySelector('.h-0')
expect(anchorDiv).toBeInTheDocument()
const observedElement = mockIntersectionObserverInstance?.observe.mock.calls[0]?.[0]
expect(observedElement).toBeInstanceOf(HTMLElement)
expect(observedElement as HTMLElement).toBeInTheDocument()
})
})
@@ -769,7 +732,7 @@ describe('List', () => {
// Arrange
const fileList = createMockFileList(2)
const props = createDefaultProps({ fileList })
const renderSpy = jest.fn()
const renderSpy = vi.fn()
// Create a wrapper component to track renders
const TestWrapper = ({ testProps }: { testProps: ListProps }) => {
@@ -832,16 +795,16 @@ describe('List', () => {
const props1 = createDefaultProps({ fileList, isLoading: false })
const props2 = createDefaultProps({ fileList, isLoading: true })
const { rerender, container } = render(<List {...props1} />)
const { rerender } = render(<List {...props1} />)
// Assert initial state - no loading spinner
expect(container.querySelector('.animation-spin')).not.toBeInTheDocument()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
// Act
rerender(<List {...props2} />)
// Assert - loading spinner should appear
expect(container.querySelector('.animation-spin')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
})
})
@@ -1003,13 +966,13 @@ describe('List', () => {
const { rerender } = render(<List {...props1} />)
// Assert initial loading state
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
// Act
rerender(<List {...props2} />)
// Assert
expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
expect(screen.getByTestId('empty-folder')).toBeInTheDocument()
})
@@ -1022,13 +985,13 @@ describe('List', () => {
const { rerender } = render(<List {...props1} />)
// Assert initial loading state
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
// Act
rerender(<List {...props2} />)
// Assert
expect(screen.queryByTestId('loading')).not.toBeInTheDocument()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
})
@@ -1038,16 +1001,16 @@ describe('List', () => {
const props1 = createDefaultProps({ isLoading: true, fileList })
const props2 = createDefaultProps({ isLoading: false, fileList })
const { rerender, container } = render(<List {...props1} />)
const { rerender } = render(<List {...props1} />)
// Assert initial partial loading state
expect(container.querySelector('.animation-spin')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
// Act
rerender(<List {...props2} />)
// Assert
expect(container.querySelector('.animation-spin')).not.toBeInTheDocument()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
})
})
@@ -1130,15 +1093,16 @@ describe('List', () => {
const props = createDefaultProps({ fileList, isLoading, keywords })
// Act
const { container } = render(<List {...props} />)
render(<List {...props} />)
// Assert
switch (expectedState) {
case 'all-loading':
expect(screen.getByTestId('loading')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
break
case 'partial-loading':
expect(container.querySelector('.animation-spin')).toBeInTheDocument()
expect(screen.getByRole('status')).toBeInTheDocument()
expect(screen.getByTestId('item-file-1')).toBeInTheDocument()
break
case 'empty-folder':
expect(screen.getByTestId('empty-folder')).toBeInTheDocument()
@@ -1179,22 +1143,9 @@ describe('List', () => {
// Accessibility Tests
// ==========================================
describe('Accessibility', () => {
it('should have proper container structure', () => {
// Arrange
const fileList = createMockFileList(2)
const props = createDefaultProps({ fileList })
// Act
const { container } = render(<List {...props} />)
// Assert - Container should be scrollable
const scrollContainer = container.querySelector('.overflow-y-auto')
expect(scrollContainer).toBeInTheDocument()
})
it('should allow interaction with reset keywords button in empty search state', () => {
// Arrange
const handleResetKeywords = jest.fn()
const handleResetKeywords = vi.fn()
const props = createDefaultProps({
fileList: [],
keywords: 'search-term',
@@ -1218,10 +1169,15 @@ describe('List', () => {
// ==========================================
describe('EmptyFolder', () => {
// Get real component for testing
const ActualEmptyFolder = jest.requireActual('./empty-folder').default
let ActualEmptyFolder: React.ComponentType
beforeAll(async () => {
const mod = await vi.importActual<{ default: React.ComponentType }>('./empty-folder')
ActualEmptyFolder = mod.default
})
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -1234,18 +1190,6 @@ describe('EmptyFolder', () => {
render(<ActualEmptyFolder />)
expect(screen.getByText(/datasetPipeline\.onlineDrive\.emptyFolder/)).toBeInTheDocument()
})
it('should render with correct container classes', () => {
const { container } = render(<ActualEmptyFolder />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex', 'size-full', 'items-center', 'justify-center')
})
it('should render text with correct styling classes', () => {
render(<ActualEmptyFolder />)
const textElement = screen.getByText(/datasetPipeline\.onlineDrive\.emptyFolder/)
expect(textElement).toHaveClass('system-xs-regular', 'text-text-tertiary')
})
})
describe('Component Memoization', () => {
@@ -1268,58 +1212,56 @@ describe('EmptyFolder', () => {
// ==========================================
describe('EmptySearchResult', () => {
// Get real component for testing
const ActualEmptySearchResult = jest.requireActual('./empty-search-result').default
let ActualEmptySearchResult: React.ComponentType<{ onResetKeywords: () => void }>
beforeAll(async () => {
const mod = await vi.importActual<{ default: React.ComponentType<{ onResetKeywords: () => void }> }>('./empty-search-result')
ActualEmptySearchResult = mod.default
})
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
it('should render without crashing', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
expect(document.body).toBeInTheDocument()
})
it('should render empty search result message', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
expect(screen.getByText(/datasetPipeline\.onlineDrive\.emptySearchResult/)).toBeInTheDocument()
})
it('should render reset keywords button', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
expect(screen.getByRole('button')).toBeInTheDocument()
expect(screen.getByText(/datasetPipeline\.onlineDrive\.resetKeywords/)).toBeInTheDocument()
})
it('should render search icon', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
const { container } = render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
const svgElement = container.querySelector('svg')
expect(svgElement).toBeInTheDocument()
})
it('should render with correct container classes', () => {
const onResetKeywords = jest.fn()
const { container } = render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('flex', 'size-full', 'flex-col', 'items-center', 'justify-center', 'gap-y-2')
})
})
describe('Props', () => {
describe('onResetKeywords prop', () => {
it('should call onResetKeywords when button is clicked', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
fireEvent.click(screen.getByRole('button'))
expect(onResetKeywords).toHaveBeenCalledTimes(1)
})
it('should call onResetKeywords on each click', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
const button = screen.getByRole('button')
fireEvent.click(button)
@@ -1338,13 +1280,13 @@ describe('EmptySearchResult', () => {
describe('Accessibility', () => {
it('should have accessible button', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
expect(screen.getByRole('button')).toBeInTheDocument()
})
it('should have readable text content', () => {
const onResetKeywords = jest.fn()
const onResetKeywords = vi.fn()
render(<ActualEmptySearchResult onResetKeywords={onResetKeywords} />)
expect(screen.getByText(/datasetPipeline\.onlineDrive\.emptySearchResult/)).toBeInTheDocument()
})
@@ -1356,10 +1298,16 @@ describe('EmptySearchResult', () => {
// ==========================================
describe('FileIcon', () => {
// Get real component for testing
const ActualFileIcon = jest.requireActual('./file-icon').default
type FileIconProps = { type: OnlineDriveFileType; fileName: string; size?: 'sm' | 'md' | 'lg' | 'xl'; className?: string }
let ActualFileIcon: React.ComponentType<FileIconProps>
beforeAll(async () => {
const mod = await vi.importActual<{ default: React.ComponentType<FileIconProps> }>('./file-icon')
ActualFileIcon = mod.default
})
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -1443,24 +1391,6 @@ describe('FileIcon', () => {
expect(container.firstChild).toBeInTheDocument()
})
})
describe('className prop', () => {
it('should apply custom className to bucket icon', () => {
const { container } = render(
<ActualFileIcon type={OnlineDriveFileType.bucket} fileName="bucket" className="custom-class" />,
)
const svg = container.querySelector('svg')
expect(svg).toHaveClass('custom-class')
})
it('should apply className to folder icon', () => {
const { container } = render(
<ActualFileIcon type={OnlineDriveFileType.folder} fileName="folder" className="folder-custom" />,
)
const svg = container.querySelector('svg')
expect(svg).toHaveClass('folder-custom')
})
})
})
describe('Icon Type Determination', () => {
@@ -1524,24 +1454,6 @@ describe('FileIcon', () => {
expect(container.firstChild).toBeInTheDocument()
})
})
describe('Styling', () => {
it('should apply default size class to bucket icon', () => {
const { container } = render(
<ActualFileIcon type={OnlineDriveFileType.bucket} fileName="bucket" />,
)
const svg = container.querySelector('svg')
expect(svg).toHaveClass('size-[18px]')
})
it('should apply default size class to folder icon', () => {
const { container } = render(
<ActualFileIcon type={OnlineDriveFileType.folder} fileName="folder" />,
)
const svg = container.querySelector('svg')
expect(svg).toHaveClass('size-[18px]')
})
})
})
// ==========================================
@@ -1549,7 +1461,7 @@ describe('FileIcon', () => {
// ==========================================
describe('Item', () => {
// Get real component for testing
const ActualItem = jest.requireActual('./item').default
let ActualItem: React.ComponentType<ItemProps>
type ItemProps = {
file: OnlineDriveFile
@@ -1560,22 +1472,26 @@ describe('Item', () => {
onOpen: (file: OnlineDriveFile) => void
}
beforeAll(async () => {
const mod = await vi.importActual<{ default: React.ComponentType<ItemProps> }>('./item')
ActualItem = mod.default
})
// Reuse createMockOnlineDriveFile from outer scope
const createItemProps = (overrides?: Partial<ItemProps>): ItemProps => ({
file: createMockOnlineDriveFile(),
isSelected: false,
onSelect: jest.fn(),
onOpen: jest.fn(),
onSelect: vi.fn(),
onOpen: vi.fn(),
...overrides,
})
// Helper to find custom checkbox element (div-based implementation)
const findCheckbox = (container: HTMLElement) => container.querySelector('[data-testid^="checkbox-"]')
// Helper to find custom radio element (div-based implementation)
const findRadio = (container: HTMLElement) => container.querySelector('.rounded-full.size-4')
const getRadio = () => screen.getByRole('radio')
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -1623,8 +1539,8 @@ describe('Item', () => {
isMultipleChoice: false,
file: createMockOnlineDriveFile({ type: OnlineDriveFileType.file }),
})
const { container } = render(<ActualItem {...props} />)
expect(findRadio(container)).toBeInTheDocument()
render(<ActualItem {...props} />)
expect(getRadio()).toBeInTheDocument()
})
it('should not render checkbox or radio for bucket type', () => {
@@ -1634,7 +1550,7 @@ describe('Item', () => {
})
const { container } = render(<ActualItem {...props} />)
expect(findCheckbox(container)).not.toBeInTheDocument()
expect(findRadio(container)).not.toBeInTheDocument()
expect(screen.queryByRole('radio')).not.toBeInTheDocument()
})
it('should render with title attribute for file name', () => {
@@ -1666,32 +1582,29 @@ describe('Item', () => {
it('should show radio as checked when isSelected is true', () => {
const props = createItemProps({ isSelected: true, isMultipleChoice: false })
const { container } = render(<ActualItem {...props} />)
const radio = findRadio(container)
// Checked radio has border-[5px] class
expect(radio).toHaveClass('border-[5px]')
render(<ActualItem {...props} />)
const radio = getRadio()
expect(radio).toHaveAttribute('aria-checked', 'true')
})
})
describe('disabled prop', () => {
it('should apply opacity class when disabled', () => {
const props = createItemProps({ disabled: true })
const { container } = render(<ActualItem {...props} />)
expect(container.querySelector('.opacity-30')).toBeInTheDocument()
})
it('should apply disabled styles to checkbox when disabled', () => {
const props = createItemProps({ disabled: true, isMultipleChoice: true })
it('should not call onSelect when clicking disabled checkbox', () => {
const onSelect = vi.fn()
const props = createItemProps({ disabled: true, isMultipleChoice: true, onSelect })
const { container } = render(<ActualItem {...props} />)
const checkbox = findCheckbox(container)
expect(checkbox).toHaveClass('cursor-not-allowed')
fireEvent.click(checkbox!)
expect(onSelect).not.toHaveBeenCalled()
})
it('should apply disabled styles to radio when disabled', () => {
const props = createItemProps({ disabled: true, isMultipleChoice: false })
const { container } = render(<ActualItem {...props} />)
const radio = findRadio(container)
expect(radio).toHaveClass('border-components-radio-border-disabled')
it('should not call onSelect when clicking disabled radio', () => {
const onSelect = vi.fn()
const props = createItemProps({ disabled: true, isMultipleChoice: false, onSelect })
render(<ActualItem {...props} />)
const radio = getRadio()
fireEvent.click(radio)
expect(onSelect).not.toHaveBeenCalled()
})
})
@@ -1707,13 +1620,13 @@ describe('Item', () => {
const props = createItemProps({ isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
expect(findCheckbox(container)).toBeInTheDocument()
expect(findRadio(container)).not.toBeInTheDocument()
expect(screen.queryByRole('radio')).not.toBeInTheDocument()
})
it('should render radio when false', () => {
const props = createItemProps({ isMultipleChoice: false })
const { container } = render(<ActualItem {...props} />)
expect(findRadio(container)).toBeInTheDocument()
expect(getRadio()).toBeInTheDocument()
expect(findCheckbox(container)).not.toBeInTheDocument()
})
})
@@ -1722,7 +1635,7 @@ describe('Item', () => {
describe('User Interactions', () => {
describe('Click on Item', () => {
it('should call onSelect when clicking on file item', () => {
const onSelect = jest.fn()
const onSelect = vi.fn()
const file = createMockOnlineDriveFile({ type: OnlineDriveFileType.file })
const props = createItemProps({ file, onSelect })
render(<ActualItem {...props} />)
@@ -1731,7 +1644,7 @@ describe('Item', () => {
})
it('should call onOpen when clicking on folder item', () => {
const onOpen = jest.fn()
const onOpen = vi.fn()
const file = createMockOnlineDriveFile({ type: OnlineDriveFileType.folder, name: 'Documents' })
const props = createItemProps({ file, onOpen })
render(<ActualItem {...props} />)
@@ -1740,7 +1653,7 @@ describe('Item', () => {
})
it('should call onOpen when clicking on bucket item', () => {
const onOpen = jest.fn()
const onOpen = vi.fn()
const file = createMockOnlineDriveFile({ type: OnlineDriveFileType.bucket, name: 'my-bucket' })
const props = createItemProps({ file, onOpen })
render(<ActualItem {...props} />)
@@ -1749,8 +1662,8 @@ describe('Item', () => {
})
it('should not call any handler when clicking disabled item', () => {
const onSelect = jest.fn()
const onOpen = jest.fn()
const onSelect = vi.fn()
const onOpen = vi.fn()
const props = createItemProps({ disabled: true, onSelect, onOpen })
render(<ActualItem {...props} />)
fireEvent.click(screen.getByText('test-file.txt'))
@@ -1761,7 +1674,7 @@ describe('Item', () => {
describe('Click on Checkbox/Radio', () => {
it('should call onSelect when clicking checkbox', () => {
const onSelect = jest.fn()
const onSelect = vi.fn()
const file = createMockOnlineDriveFile()
const props = createItemProps({ file, onSelect, isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
@@ -1771,17 +1684,17 @@ describe('Item', () => {
})
it('should call onSelect when clicking radio', () => {
const onSelect = jest.fn()
const onSelect = vi.fn()
const file = createMockOnlineDriveFile()
const props = createItemProps({ file, onSelect, isMultipleChoice: false })
const { container } = render(<ActualItem {...props} />)
const radio = findRadio(container)
fireEvent.click(radio!)
render(<ActualItem {...props} />)
const radio = getRadio()
fireEvent.click(radio)
expect(onSelect).toHaveBeenCalledWith(file)
})
it('should stop event propagation when clicking checkbox', () => {
const onSelect = jest.fn()
const onSelect = vi.fn()
const file = createMockOnlineDriveFile()
const props = createItemProps({ file, onSelect, isMultipleChoice: true })
const { container } = render(<ActualItem {...props} />)
@@ -1832,58 +1745,6 @@ describe('Item', () => {
expect(screen.getByText('5.00 GB')).toBeInTheDocument()
})
})
describe('Styling', () => {
it('should have cursor-pointer class', () => {
const props = createItemProps()
const { container } = render(<ActualItem {...props} />)
expect(container.firstChild).toHaveClass('cursor-pointer')
})
it('should have hover class', () => {
const props = createItemProps()
const { container } = render(<ActualItem {...props} />)
expect(container.firstChild).toHaveClass('hover:bg-state-base-hover')
})
it('should truncate file name', () => {
const props = createItemProps()
render(<ActualItem {...props} />)
const nameElement = screen.getByText('test-file.txt')
expect(nameElement).toHaveClass('truncate')
})
})
describe('Prop Variations', () => {
it.each([
{ isSelected: true, isMultipleChoice: true, disabled: false },
{ isSelected: true, isMultipleChoice: false, disabled: false },
{ isSelected: false, isMultipleChoice: true, disabled: false },
{ isSelected: false, isMultipleChoice: false, disabled: false },
{ isSelected: true, isMultipleChoice: true, disabled: true },
{ isSelected: false, isMultipleChoice: false, disabled: true },
])('should render with isSelected=$isSelected, isMultipleChoice=$isMultipleChoice, disabled=$disabled',
({ isSelected, isMultipleChoice, disabled }) => {
const props = createItemProps({ isSelected, isMultipleChoice, disabled })
const { container } = render(<ActualItem {...props} />)
if (isMultipleChoice) {
const checkbox = findCheckbox(container)
expect(checkbox).toBeInTheDocument()
if (isSelected)
expect(checkbox?.querySelector('[data-testid^="check-icon-"]')).toBeInTheDocument()
if (disabled)
expect(checkbox).toHaveClass('cursor-not-allowed')
}
else {
const radio = findRadio(container)
expect(radio).toBeInTheDocument()
if (isSelected)
expect(radio).toHaveClass('border-[5px]')
if (disabled)
expect(radio).toHaveClass('border-components-radio-border-disabled')
}
})
})
})
// ==========================================
@@ -1891,8 +1752,17 @@ describe('Item', () => {
// ==========================================
describe('utils', () => {
// Import actual utils functions
const { getFileExtension, getFileType } = jest.requireActual('./utils')
const { FileAppearanceTypeEnum } = jest.requireActual('@/app/components/base/file-uploader/types')
let getFileExtension: (filename: string) => string
let getFileType: (filename: string) => string
let FileAppearanceTypeEnum: Record<string, string>
beforeAll(async () => {
const utils = await vi.importActual<{ getFileExtension: typeof getFileExtension; getFileType: typeof getFileType }>('./utils')
const types = await vi.importActual<{ FileAppearanceTypeEnum: typeof FileAppearanceTypeEnum }>('@/app/components/base/file-uploader/types')
getFileExtension = utils.getFileExtension
getFileType = utils.getFileType
FileAppearanceTypeEnum = types.FileAppearanceTypeEnum
})
describe('getFileExtension', () => {
describe('Basic Functionality', () => {

View File

@@ -1,4 +1,5 @@
import React, { useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import type { OnlineDriveFile } from '@/models/pipeline'
import Item from './item'
import EmptyFolder from './empty-folder'
@@ -28,6 +29,7 @@ const List = ({
isLoading,
supportBatchUpload,
}: FileListProps) => {
const { t } = useTranslation()
const anchorRef = useRef<HTMLDivElement>(null)
const observerRef = useRef<IntersectionObserver>(null)
const dataSourceStore = useDataSourceStore()
@@ -87,7 +89,12 @@ const List = ({
}
{
isPartialLoading && (
<div className='flex items-center justify-center py-2'>
<div
className='flex items-center justify-center py-2'
role='status'
aria-live='polite'
aria-label={t('appApi.loading')}
>
<RiLoader2Line className='animation-spin size-4 text-text-tertiary' />
</div>
)

View File

@@ -13,44 +13,53 @@ import type { OnlineDriveData } from '@/types/pipeline'
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock useDocLink - context hook requires mocking
const mockDocLink = jest.fn((path?: string) => `https://docs.example.com${path || ''}`)
jest.mock('@/context/i18n', () => ({
const mockDocLink = vi.fn((path?: string) => `https://docs.example.com${path || ''}`)
vi.mock('@/context/i18n', () => ({
useDocLink: () => mockDocLink,
}))
// Mock dataset-detail context - context provider requires mocking
let mockPipelineId: string | undefined = 'pipeline-123'
jest.mock('@/context/dataset-detail', () => ({
vi.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', () => ({
const mockSetShowAccountSettingModal = vi.fn()
vi.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),
const { mockSsePost } = vi.hoisted(() => ({
mockSsePost: vi.fn(),
}))
vi.mock('@/service/base', () => ({
ssePost: mockSsePost,
}))
// Mock useGetDataSourceAuth - API service hook requires mocking
const mockUseGetDataSourceAuth = jest.fn()
jest.mock('@/service/use-datasource', () => ({
useGetDataSourceAuth: (params: any) => mockUseGetDataSourceAuth(params),
const { mockUseGetDataSourceAuth } = vi.hoisted(() => ({
mockUseGetDataSourceAuth: vi.fn(),
}))
vi.mock('@/service/use-datasource', () => ({
useGetDataSourceAuth: mockUseGetDataSourceAuth,
}))
// Mock Toast
const mockToastNotify = jest.fn()
jest.mock('@/app/components/base/toast', () => ({
const { mockToastNotify } = vi.hoisted(() => ({
mockToastNotify: vi.fn(),
}))
vi.mock('@/app/components/base/toast', () => ({
__esModule: true,
default: {
notify: (...args: any[]) => mockToastNotify(...args),
notify: mockToastNotify,
},
}))
@@ -68,26 +77,26 @@ const mockStoreState = {
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(),
setOnlineDriveFileList: vi.fn(),
setKeywords: vi.fn(),
setSelectedFileIds: vi.fn(),
setBreadcrumbs: vi.fn(),
setPrefix: vi.fn(),
setBucket: vi.fn(),
setHasBucket: vi.fn(),
}
const mockGetState = jest.fn(() => mockStoreState)
const mockGetState = vi.fn(() => mockStoreState)
const mockDataSourceStore = { getState: mockGetState }
jest.mock('../store', () => ({
vi.mock('../store', () => ({
useDataSourceStoreWithSelector: (selector: (s: any) => any) => selector(mockStoreState),
useDataSourceStore: () => mockDataSourceStore,
}))
// Mock Header component
jest.mock('../base/header', () => {
const MockHeader = (props: any) => (
vi.mock('../base/header', () => ({
default: (props: any) => (
<div data-testid="header">
<span data-testid="header-doc-title">{props.docTitle}</span>
<span data-testid="header-doc-link">{props.docLink}</span>
@@ -97,13 +106,12 @@ jest.mock('../base/header', () => {
<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) => (
vi.mock('./file-list', () => ({
default: (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>
@@ -164,9 +172,8 @@ jest.mock('./file-list', () => {
Open File
</button>
</div>
)
return MockFileList
})
),
}))
// ==========================================
// Test Data Builders
@@ -206,7 +213,7 @@ type OnlineDriveProps = React.ComponentProps<typeof OnlineDrive>
const createDefaultProps = (overrides?: Partial<OnlineDriveProps>): OnlineDriveProps => ({
nodeId: 'node-1',
nodeData: createMockNodeData(),
onCredentialChange: jest.fn(),
onCredentialChange: vi.fn(),
isInPipeline: false,
supportBatchUpload: true,
...overrides,
@@ -226,13 +233,13 @@ const resetMockStoreState = () => {
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()
mockStoreState.setOnlineDriveFileList = vi.fn()
mockStoreState.setKeywords = vi.fn()
mockStoreState.setSelectedFileIds = vi.fn()
mockStoreState.setBreadcrumbs = vi.fn()
mockStoreState.setPrefix = vi.fn()
mockStoreState.setBucket = vi.fn()
mockStoreState.setHasBucket = vi.fn()
}
// ==========================================
@@ -240,7 +247,7 @@ const resetMockStoreState = () => {
// ==========================================
describe('OnlineDrive', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Reset store state
resetMockStoreState()
@@ -498,7 +505,7 @@ describe('OnlineDrive', () => {
describe('onCredentialChange prop', () => {
it('should call onCredentialChange with credential id', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
// Act
@@ -847,7 +854,7 @@ describe('OnlineDrive', () => {
describe('Credential Change', () => {
it('should call onCredentialChange prop', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
render(<OnlineDrive {...props} />)
@@ -1296,14 +1303,14 @@ describe('OnlineDrive', () => {
// ==========================================
describe('Header', () => {
const createHeaderProps = (overrides?: Partial<React.ComponentProps<typeof Header>>) => ({
onClickConfiguration: jest.fn(),
onClickConfiguration: vi.fn(),
docTitle: 'Documentation',
docLink: 'https://docs.example.com/guide',
...overrides,
})
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -1398,7 +1405,7 @@ describe('Header', () => {
describe('onClickConfiguration prop', () => {
it('should call onClickConfiguration when configuration icon is clicked', () => {
// Arrange
const mockOnClickConfiguration = jest.fn()
const mockOnClickConfiguration = vi.fn()
const props = createHeaderProps({ onClickConfiguration: mockOnClickConfiguration })
// Act

View File

@@ -34,12 +34,12 @@ const createMockCrawlResultItems = (count = 3): CrawlResultItemType[] => {
describe('CheckboxWithLabel', () => {
const defaultProps = {
isChecked: false,
onChange: jest.fn(),
onChange: vi.fn(),
label: 'Test Label',
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -114,7 +114,7 @@ describe('CheckboxWithLabel', () => {
describe('User Interactions', () => {
it('should call onChange with true when clicking unchecked checkbox', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={false} onChange={mockOnChange} />)
// Act
@@ -127,7 +127,7 @@ describe('CheckboxWithLabel', () => {
it('should call onChange with false when clicking checked checkbox', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
const { container } = render(<CheckboxWithLabel {...defaultProps} isChecked={true} onChange={mockOnChange} />)
// Act
@@ -140,7 +140,7 @@ describe('CheckboxWithLabel', () => {
it('should not trigger onChange when clicking label text due to custom checkbox', () => {
// Arrange
const mockOnChange = jest.fn()
const mockOnChange = vi.fn()
render(<CheckboxWithLabel {...defaultProps} onChange={mockOnChange} />)
// Act - Click on the label text element
@@ -160,14 +160,14 @@ describe('CrawledResultItem', () => {
const defaultProps = {
payload: createMockCrawlResultItem(),
isChecked: false,
onCheckChange: jest.fn(),
onCheckChange: vi.fn(),
isPreview: false,
showPreview: true,
onPreview: jest.fn(),
onPreview: vi.fn(),
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -282,7 +282,7 @@ describe('CrawledResultItem', () => {
describe('User Interactions', () => {
it('should call onCheckChange with true when clicking unchecked checkbox', () => {
// Arrange
const mockOnCheckChange = jest.fn()
const mockOnCheckChange = vi.fn()
const { container } = render(
<CrawledResultItem
{...defaultProps}
@@ -301,7 +301,7 @@ describe('CrawledResultItem', () => {
it('should call onCheckChange with false when clicking checked checkbox', () => {
// Arrange
const mockOnCheckChange = jest.fn()
const mockOnCheckChange = vi.fn()
const { container } = render(
<CrawledResultItem
{...defaultProps}
@@ -320,7 +320,7 @@ describe('CrawledResultItem', () => {
it('should call onPreview when clicking preview button', () => {
// Arrange
const mockOnPreview = jest.fn()
const mockOnPreview = vi.fn()
render(<CrawledResultItem {...defaultProps} onPreview={mockOnPreview} />)
// Act
@@ -332,7 +332,7 @@ describe('CrawledResultItem', () => {
it('should toggle radio state when isMultipleChoice is false', () => {
// Arrange
const mockOnCheckChange = jest.fn()
const mockOnCheckChange = vi.fn()
const { container } = render(
<CrawledResultItem
{...defaultProps}
@@ -359,12 +359,12 @@ describe('CrawledResult', () => {
const defaultProps = {
list: createMockCrawlResultItems(3),
checkedList: [] as CrawlResultItemType[],
onSelectedChange: jest.fn(),
onSelectedChange: vi.fn(),
usedTime: 1.5,
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -478,7 +478,7 @@ describe('CrawledResult', () => {
describe('User Interactions', () => {
it('should call onSelectedChange with all items when clicking select all', () => {
// Arrange
const mockOnSelectedChange = jest.fn()
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
<CrawledResult
@@ -499,7 +499,7 @@ describe('CrawledResult', () => {
it('should call onSelectedChange with empty array when clicking reset all', () => {
// Arrange
const mockOnSelectedChange = jest.fn()
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
<CrawledResult
@@ -520,7 +520,7 @@ describe('CrawledResult', () => {
it('should add item to checkedList when checking unchecked item', () => {
// Arrange
const mockOnSelectedChange = jest.fn()
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
<CrawledResult
@@ -541,7 +541,7 @@ describe('CrawledResult', () => {
it('should remove item from checkedList when unchecking checked item', () => {
// Arrange
const mockOnSelectedChange = jest.fn()
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
<CrawledResult
@@ -562,7 +562,7 @@ describe('CrawledResult', () => {
it('should replace selection when checking in single choice mode', () => {
// Arrange
const mockOnSelectedChange = jest.fn()
const mockOnSelectedChange = vi.fn()
const list = createMockCrawlResultItems(3)
const { container } = render(
<CrawledResult
@@ -584,7 +584,7 @@ describe('CrawledResult', () => {
it('should call onPreview with item and index when clicking preview', () => {
// Arrange
const mockOnPreview = jest.fn()
const mockOnPreview = vi.fn()
const list = createMockCrawlResultItems(3)
render(
<CrawledResult
@@ -664,7 +664,7 @@ describe('Crawling', () => {
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -753,7 +753,7 @@ describe('ErrorMessage', () => {
}
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
})
describe('Rendering', () => {
@@ -883,7 +883,7 @@ describe('Base Components Integration', () => {
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={jest.fn()}
onSelectedChange={vi.fn()}
usedTime={1.0}
/>,
)
@@ -902,7 +902,7 @@ describe('Base Components Integration', () => {
<CrawledResult
list={list}
checkedList={[]}
onSelectedChange={jest.fn()}
onSelectedChange={vi.fn()}
usedTime={1.0}
isMultipleChoice={true}
/>,
@@ -916,8 +916,8 @@ describe('Base Components Integration', () => {
it('should allow selecting and previewing items', () => {
// Arrange
const list = createMockCrawlResultItems(3)
const mockOnSelectedChange = jest.fn()
const mockOnPreview = jest.fn()
const mockOnSelectedChange = vi.fn()
const mockOnPreview = vi.fn()
const { container } = render(
<CrawledResult

View File

@@ -1,3 +1,4 @@
import type { MockInstance } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import Options from './index'
@@ -11,19 +12,22 @@ import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/ty
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock useInitialData and useConfigurations hooks
const mockUseInitialData = jest.fn()
const mockUseConfigurations = jest.fn()
jest.mock('@/app/components/rag-pipeline/hooks/use-input-fields', () => ({
useInitialData: (...args: any[]) => mockUseInitialData(...args),
useConfigurations: (...args: any[]) => mockUseConfigurations(...args),
const { mockUseInitialData, mockUseConfigurations } = vi.hoisted(() => ({
mockUseInitialData: vi.fn(),
mockUseConfigurations: vi.fn(),
}))
vi.mock('@/app/components/rag-pipeline/hooks/use-input-fields', () => ({
useInitialData: mockUseInitialData,
useConfigurations: mockUseConfigurations,
}))
// Mock BaseField
const mockBaseField = jest.fn()
jest.mock('@/app/components/base/form/form-scenarios/base/field', () => {
const mockBaseField = vi.fn()
vi.mock('@/app/components/base/form/form-scenarios/base/field', () => {
const MockBaseFieldFactory = (props: any) => {
mockBaseField(props)
const MockField = ({ form }: { form: any }) => (
@@ -38,13 +42,13 @@ jest.mock('@/app/components/base/form/form-scenarios/base/field', () => {
)
return MockField
}
return MockBaseFieldFactory
return { default: MockBaseFieldFactory }
})
// Mock useAppForm
const mockHandleSubmit = jest.fn()
const mockHandleSubmit = vi.fn()
const mockFormValues: Record<string, any> = {}
jest.mock('@/app/components/base/form', () => ({
vi.mock('@/app/components/base/form', () => ({
useAppForm: (options: any) => {
const formOptions = options
return {
@@ -106,7 +110,7 @@ const createDefaultProps = (overrides?: Partial<OptionsProps>): OptionsProps =>
variables: createMockVariables(),
step: CrawlStep.init,
runDisabled: false,
onSubmit: jest.fn(),
onSubmit: vi.fn(),
...overrides,
})
@@ -114,13 +118,13 @@ const createDefaultProps = (overrides?: Partial<OptionsProps>): OptionsProps =>
// Test Suites
// ==========================================
describe('Options', () => {
let toastNotifySpy: jest.SpyInstance
let toastNotifySpy: MockInstance
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Spy on Toast.notify instead of mocking the entire module
toastNotifySpy = jest.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: jest.fn() }))
toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
// Reset mock form values
Object.keys(mockFormValues).forEach(key => delete mockFormValues[key])
@@ -379,7 +383,7 @@ describe('Options', () => {
type: BaseFieldType.textInput,
})
mockUseConfigurations.mockReturnValue([config])
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
// Act
@@ -392,7 +396,7 @@ describe('Options', () => {
it('should not call onSubmit when validation fails', () => {
// Arrange
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
// Create a required field configuration
const requiredConfig = createMockConfiguration({
variable: 'url',
@@ -421,7 +425,7 @@ describe('Options', () => {
mockUseConfigurations.mockReturnValue(configs)
mockFormValues.url = 'https://example.com'
mockFormValues.depth = 2
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
// Act
@@ -591,7 +595,7 @@ describe('Options', () => {
required: false, // Not required so validation passes with empty value
})
mockUseConfigurations.mockReturnValue([config])
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
@@ -635,8 +639,8 @@ describe('Options', () => {
// Act
const form = container.querySelector('form')!
const mockPreventDefault = jest.fn()
const mockStopPropagation = jest.fn()
const mockPreventDefault = vi.fn()
const mockStopPropagation = vi.fn()
fireEvent.submit(form, {
preventDefault: mockPreventDefault,
@@ -655,7 +659,7 @@ describe('Options', () => {
type: BaseFieldType.textInput,
})
mockUseConfigurations.mockReturnValue([config])
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
@@ -668,7 +672,7 @@ describe('Options', () => {
it('should not trigger submit when button is disabled', () => {
// Arrange
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit, runDisabled: true })
render(<Options {...props} />)
@@ -837,7 +841,7 @@ describe('Options', () => {
})
mockUseConfigurations.mockReturnValue([requiredConfig])
mockFormValues.url = 'https://example.com' // Provide valid value
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
@@ -947,7 +951,7 @@ describe('Options', () => {
type: BaseFieldType.textInput,
})
mockUseConfigurations.mockReturnValue([config])
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)
@@ -968,7 +972,7 @@ describe('Options', () => {
type: BaseFieldType.textInput,
})
mockUseConfigurations.mockReturnValue([config])
const mockOnSubmit = jest.fn()
const mockOnSubmit = vi.fn()
const props = createDefaultProps({ onSubmit: mockOnSubmit })
render(<Options {...props} />)

View File

@@ -10,44 +10,53 @@ import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/con
// Mock Modules
// ==========================================
// Note: react-i18next uses global mock from web/__mocks__/react-i18next.ts
// Note: react-i18next uses global mock from web/vitest.setup.ts
// Mock useDocLink - context hook requires mocking
const mockDocLink = jest.fn((path?: string) => `https://docs.example.com${path || ''}`)
jest.mock('@/context/i18n', () => ({
const mockDocLink = vi.fn((path?: string) => `https://docs.example.com${path || ''}`)
vi.mock('@/context/i18n', () => ({
useDocLink: () => mockDocLink,
}))
// Mock dataset-detail context - context provider requires mocking
let mockPipelineId: string | undefined = 'pipeline-123'
jest.mock('@/context/dataset-detail', () => ({
vi.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', () => ({
const mockSetShowAccountSettingModal = vi.fn()
vi.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),
const { mockSsePost } = vi.hoisted(() => ({
mockSsePost: vi.fn(),
}))
vi.mock('@/service/base', () => ({
ssePost: mockSsePost,
}))
// Mock useGetDataSourceAuth - API service hook requires mocking
const mockUseGetDataSourceAuth = jest.fn()
jest.mock('@/service/use-datasource', () => ({
useGetDataSourceAuth: (params: any) => mockUseGetDataSourceAuth(params),
const { mockUseGetDataSourceAuth } = vi.hoisted(() => ({
mockUseGetDataSourceAuth: vi.fn(),
}))
vi.mock('@/service/use-datasource', () => ({
useGetDataSourceAuth: mockUseGetDataSourceAuth,
}))
// Mock usePipeline hooks - API service hooks require mocking
const mockUseDraftPipelinePreProcessingParams = jest.fn()
const mockUsePublishedPipelinePreProcessingParams = jest.fn()
jest.mock('@/service/use-pipeline', () => ({
useDraftPipelinePreProcessingParams: (...args: any[]) => mockUseDraftPipelinePreProcessingParams(...args),
usePublishedPipelinePreProcessingParams: (...args: any[]) => mockUsePublishedPipelinePreProcessingParams(...args),
const { mockUseDraftPipelinePreProcessingParams, mockUsePublishedPipelinePreProcessingParams } = vi.hoisted(() => ({
mockUseDraftPipelinePreProcessingParams: vi.fn(),
mockUsePublishedPipelinePreProcessingParams: vi.fn(),
}))
vi.mock('@/service/use-pipeline', () => ({
useDraftPipelinePreProcessingParams: mockUseDraftPipelinePreProcessingParams,
usePublishedPipelinePreProcessingParams: mockUsePublishedPipelinePreProcessingParams,
}))
// Note: zustand/react/shallow useShallow is imported directly (simple utility function)
@@ -59,24 +68,24 @@ const mockStoreState = {
websitePages: [] as CrawlResultItem[],
previewIndex: -1,
currentCredentialId: '',
setWebsitePages: jest.fn(),
setCurrentWebsite: jest.fn(),
setPreviewIndex: jest.fn(),
setStep: jest.fn(),
setCrawlResult: jest.fn(),
setWebsitePages: vi.fn(),
setCurrentWebsite: vi.fn(),
setPreviewIndex: vi.fn(),
setStep: vi.fn(),
setCrawlResult: vi.fn(),
}
const mockGetState = jest.fn(() => mockStoreState)
const mockGetState = vi.fn(() => mockStoreState)
const mockDataSourceStore = { getState: mockGetState }
jest.mock('../store', () => ({
vi.mock('../store', () => ({
useDataSourceStoreWithSelector: (selector: (s: any) => any) => selector(mockStoreState),
useDataSourceStore: () => mockDataSourceStore,
}))
// Mock Header component
jest.mock('../base/header', () => {
const MockHeader = (props: any) => (
vi.mock('../base/header', () => ({
default: (props: any) => (
<div data-testid="header">
<span data-testid="header-doc-title">{props.docTitle}</span>
<span data-testid="header-doc-link">{props.docLink}</span>
@@ -86,14 +95,13 @@ jest.mock('../base/header', () => {
<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 Options component
const mockOptionsSubmit = jest.fn()
jest.mock('./base/options', () => {
const MockOptions = (props: any) => (
const mockOptionsSubmit = vi.fn()
vi.mock('./base/options', () => ({
default: (props: any) => (
<div data-testid="options">
<span data-testid="options-step">{props.step}</span>
<span data-testid="options-run-disabled">{String(props.runDisabled)}</span>
@@ -108,35 +116,32 @@ jest.mock('./base/options', () => {
Submit
</button>
</div>
)
return MockOptions
})
),
}))
// Mock Crawling component
jest.mock('./base/crawling', () => {
const MockCrawling = (props: any) => (
vi.mock('./base/crawling', () => ({
default: (props: any) => (
<div data-testid="crawling">
<span data-testid="crawling-crawled-num">{props.crawledNum}</span>
<span data-testid="crawling-total-num">{props.totalNum}</span>
</div>
)
return MockCrawling
})
),
}))
// Mock ErrorMessage component
jest.mock('./base/error-message', () => {
const MockErrorMessage = (props: any) => (
vi.mock('./base/error-message', () => ({
default: (props: any) => (
<div data-testid="error-message" className={props.className}>
<span data-testid="error-title">{props.title}</span>
<span data-testid="error-msg">{props.errorMsg}</span>
</div>
)
return MockErrorMessage
})
),
}))
// Mock CrawledResult component
jest.mock('./base/crawled-result', () => {
const MockCrawledResult = (props: any) => (
vi.mock('./base/crawled-result', () => ({
default: (props: any) => (
<div data-testid="crawled-result" className={props.className}>
<span data-testid="crawled-result-count">{props.list?.length || 0}</span>
<span data-testid="crawled-result-checked-count">{props.checkedList?.length || 0}</span>
@@ -157,9 +162,8 @@ jest.mock('./base/crawled-result', () => {
Preview
</button>
</div>
)
return MockCrawledResult
})
),
}))
// ==========================================
// Test Data Builders
@@ -199,7 +203,7 @@ type WebsiteCrawlProps = React.ComponentProps<typeof WebsiteCrawl>
const createDefaultProps = (overrides?: Partial<WebsiteCrawlProps>): WebsiteCrawlProps => ({
nodeId: 'node-1',
nodeData: createMockNodeData(),
onCredentialChange: jest.fn(),
onCredentialChange: vi.fn(),
isInPipeline: false,
supportBatchUpload: true,
...overrides,
@@ -210,7 +214,7 @@ const createDefaultProps = (overrides?: Partial<WebsiteCrawlProps>): WebsiteCraw
// ==========================================
describe('WebsiteCrawl', () => {
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
// Reset store state
mockStoreState.crawlResult = undefined
@@ -218,11 +222,11 @@ describe('WebsiteCrawl', () => {
mockStoreState.websitePages = []
mockStoreState.previewIndex = -1
mockStoreState.currentCredentialId = ''
mockStoreState.setWebsitePages = jest.fn()
mockStoreState.setCurrentWebsite = jest.fn()
mockStoreState.setPreviewIndex = jest.fn()
mockStoreState.setStep = jest.fn()
mockStoreState.setCrawlResult = jest.fn()
mockStoreState.setWebsitePages = vi.fn()
mockStoreState.setCurrentWebsite = vi.fn()
mockStoreState.setPreviewIndex = vi.fn()
mockStoreState.setStep = vi.fn()
mockStoreState.setCrawlResult = vi.fn()
// Reset context values
mockPipelineId = 'pipeline-123'
@@ -511,7 +515,7 @@ describe('WebsiteCrawl', () => {
describe('onCredentialChange prop', () => {
it('should call onCredentialChange with credential id and reset state', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
// Act
@@ -684,7 +688,7 @@ describe('WebsiteCrawl', () => {
it('should have stable handleCredentialChange that resets state', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
render(<WebsiteCrawl {...props} />)
@@ -732,7 +736,7 @@ describe('WebsiteCrawl', () => {
it('should handle credential change', () => {
// Arrange
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
render(<WebsiteCrawl {...props} />)
@@ -1263,7 +1267,7 @@ describe('WebsiteCrawl', () => {
const props: WebsiteCrawlProps = {
nodeId: 'node-1',
nodeData: createMockNodeData(),
onCredentialChange: jest.fn(),
onCredentialChange: vi.fn(),
// isInPipeline and supportBatchUpload are not provided
}
@@ -1399,7 +1403,7 @@ describe('WebsiteCrawl', () => {
it('should handle credential change and allow new crawl', () => {
// Arrange
mockStoreState.currentCredentialId = 'initial-cred'
const mockOnCredentialChange = jest.fn()
const mockOnCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange: mockOnCredentialChange })
// Act
@@ -1453,7 +1457,7 @@ describe('WebsiteCrawl', () => {
it('should not re-run callbacks when props are the same', () => {
// Arrange
const onCredentialChange = jest.fn()
const onCredentialChange = vi.fn()
const props = createDefaultProps({ onCredentialChange })
// Act