test: enhance dataset card and rag pipeline modal tests with additional mocks and timeout adjustments

This commit is contained in:
CodingOnStar
2026-02-04 09:54:16 +08:00
parent f83bf388d5
commit 3c0749bb53
4 changed files with 137 additions and 87 deletions

View File

@ -6,6 +6,13 @@ import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datase
import { RETRIEVE_METHOD } from '@/types/app'
import DatasetCardHeader from './dataset-card-header'
// Mock AppIcon component to avoid emoji-mart initialization issues
vi.mock('@/app/components/base/app-icon', () => ({
default: ({ icon, className }: { icon?: string, className?: string }) => (
<div data-testid="app-icon" className={className}>{icon}</div>
),
}))
// Mock useFormatTimeFromNow hook
vi.mock('@/hooks/use-format-time-from-now', () => ({
useFormatTimeFromNow: () => ({

View File

@ -19,6 +19,28 @@ vi.mock('../../../rename-modal', () => ({
),
}))
// Mock Confirm component since it uses createPortal which can cause issues in tests
vi.mock('@/app/components/base/confirm', () => ({
default: ({ isShow, title, content, onConfirm, onCancel }: {
isShow: boolean
title: string
content?: React.ReactNode
onConfirm: () => void
onCancel: () => void
}) => (
isShow
? (
<div data-testid="confirm-modal">
<div data-testid="confirm-title">{title}</div>
<div data-testid="confirm-content">{content}</div>
<button onClick={onCancel} role="button" aria-label="cancel">Cancel</button>
<button onClick={onConfirm} role="button" aria-label="confirm">Confirm</button>
</div>
)
: null
),
}))
describe('DatasetCardModals', () => {
const mockDataset: DataSet = {
id: 'dataset-1',
@ -172,11 +194,9 @@ describe('DatasetCardModals', () => {
/>,
)
// Find and click the confirm button
const confirmButton = screen.getByRole('button', { name: /confirm|ok|delete/i })
|| screen.getAllByRole('button').find(btn => btn.textContent?.toLowerCase().includes('confirm'))
if (confirmButton)
fireEvent.click(confirmButton)
// Find and click the confirm button using our mocked Confirm component
const confirmButton = screen.getByRole('button', { name: /confirm/i })
fireEvent.click(confirmButton)
expect(onConfirmDelete).toHaveBeenCalledTimes(1)
})

View File

@ -7,47 +7,72 @@ import RagPipelinePanel from './index'
// Mock External Dependencies
// ============================================================================
// Type definitions for dynamic module
type DynamicModule = {
default?: React.ComponentType<Record<string, unknown>>
}
// Mock reactflow to avoid zustand provider error
vi.mock('reactflow', () => ({
useNodes: () => [],
useStoreApi: () => ({
getState: () => ({
getNodes: () => [],
}),
}),
useReactFlow: () => ({
getNodes: () => [],
}),
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
const state = {
getNodes: () => [],
}
return selector(state)
},
}))
type PromiseOrModule = Promise<DynamicModule> | DynamicModule
// Use vi.hoisted to create variables that can be used in vi.mock
const { dynamicMocks, mockInputFieldEditorProps } = vi.hoisted(() => {
let counter = 0
const mockInputFieldEditorProps = vi.fn()
// Mock next/dynamic to return synchronous components immediately
const createMockComponent = () => {
const index = counter++
// Order matches the imports in index.tsx:
// 0: Record
// 1: TestRunPanel
// 2: InputFieldPanel
// 3: InputFieldEditorPanel
// 4: PreviewPanel
// 5: GlobalVariablePanel
switch (index) {
case 0:
return () => <div data-testid="record-panel">Record Panel</div>
case 1:
return () => <div data-testid="test-run-panel">Test Run Panel</div>
case 2:
return () => <div data-testid="input-field-panel">Input Field Panel</div>
case 3:
return (props: Record<string, unknown>) => {
mockInputFieldEditorProps(props)
return <div data-testid="input-field-editor-panel">Input Field Editor Panel</div>
}
case 4:
return () => <div data-testid="preview-panel">Preview Panel</div>
case 5:
return () => <div data-testid="global-variable-panel">Global Variable Panel</div>
default:
return () => (
<div data-testid="dynamic-fallback">
Dynamic Component
{index}
</div>
)
}
}
return { dynamicMocks: { createMockComponent }, mockInputFieldEditorProps }
})
// Mock next/dynamic
vi.mock('next/dynamic', () => ({
default: (loader: () => PromiseOrModule, _options?: Record<string, unknown>) => {
let Component: React.ComponentType<Record<string, unknown>> | null = null
// Try to resolve the loader synchronously for mocked modules
try {
const result = loader() as PromiseOrModule
if (result && typeof (result as Promise<DynamicModule>).then === 'function') {
// For async modules, we need to handle them specially
// This will work with vi.mock since mocks resolve synchronously
(result as Promise<DynamicModule>).then((mod: DynamicModule) => {
Component = (mod.default || mod) as React.ComponentType<Record<string, unknown>>
})
}
else if (result) {
Component = ((result as DynamicModule).default || result) as React.ComponentType<Record<string, unknown>>
}
}
catch {
// If the module can't be resolved, Component stays null
}
// Return a simple wrapper that renders the component or null
const DynamicComponent = React.forwardRef((props: Record<string, unknown>, ref: React.Ref<unknown>) => {
// For mocked modules, Component should already be set
if (Component)
return <Component {...props} ref={ref} />
return null
})
DynamicComponent.displayName = 'DynamicComponent'
return DynamicComponent
default: (_loader: () => Promise<{ default: React.ComponentType }>, _options?: Record<string, unknown>) => {
return dynamicMocks.createMockComponent()
},
}))
@ -68,6 +93,28 @@ type MockStoreState = {
showInputFieldPreviewPanel: boolean
inputFieldEditPanelProps: Record<string, unknown> | null
pipelineId: string
nodePanelWidth: number
workflowCanvasWidth: number
otherPanelWidth: number
setShowInputFieldPanel?: (show: boolean) => void
setShowInputFieldPreviewPanel?: (show: boolean) => void
setInputFieldEditPanelProps?: (props: Record<string, unknown> | null) => void
}
const mockWorkflowStoreState: MockStoreState = {
historyWorkflowData: null,
showDebugAndPreviewPanel: false,
showGlobalVariablePanel: false,
showInputFieldPanel: false,
showInputFieldPreviewPanel: false,
inputFieldEditPanelProps: null,
pipelineId: 'test-pipeline-123',
nodePanelWidth: 400,
workflowCanvasWidth: 1200,
otherPanelWidth: 0,
setShowInputFieldPanel: vi.fn(),
setShowInputFieldPreviewPanel: vi.fn(),
setInputFieldEditPanelProps: vi.fn(),
}
vi.mock('@/app/components/workflow/store', () => ({
@ -80,9 +127,15 @@ vi.mock('@/app/components/workflow/store', () => ({
showInputFieldPreviewPanel: mockShowInputFieldPreviewPanel,
inputFieldEditPanelProps: mockInputFieldEditPanelProps,
pipelineId: mockPipelineId,
nodePanelWidth: 400,
workflowCanvasWidth: 1200,
otherPanelWidth: 0,
}
return selector(state)
},
useWorkflowStore: () => ({
getState: () => mockWorkflowStoreState,
}),
}))
// Mock Panel component to capture props and render children
@ -99,40 +152,6 @@ vi.mock('@/app/components/workflow/panel', () => ({
},
}))
// Mock Record component
vi.mock('@/app/components/workflow/panel/record', () => ({
default: () => <div data-testid="record-panel">Record Panel</div>,
}))
// Mock TestRunPanel component
vi.mock('@/app/components/rag-pipeline/components/panel/test-run', () => ({
default: () => <div data-testid="test-run-panel">Test Run Panel</div>,
}))
// Mock InputFieldPanel component
vi.mock('./input-field', () => ({
default: () => <div data-testid="input-field-panel">Input Field Panel</div>,
}))
// Mock InputFieldEditorPanel component
const mockInputFieldEditorProps = vi.fn()
vi.mock('./input-field/editor', () => ({
default: (props: Record<string, unknown>) => {
mockInputFieldEditorProps(props)
return <div data-testid="input-field-editor-panel">Input Field Editor Panel</div>
},
}))
// Mock PreviewPanel component
vi.mock('./input-field/preview', () => ({
default: () => <div data-testid="preview-panel">Preview Panel</div>,
}))
// Mock GlobalVariablePanel component
vi.mock('@/app/components/workflow/panel/global-variable-panel', () => ({
default: () => <div data-testid="global-variable-panel">Global Variable Panel</div>,
}))
// ============================================================================
// Helper Functions
// ============================================================================

View File

@ -552,11 +552,15 @@ describe('UpdateDSLModal', () => {
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
fireEvent.change(fileInput, { target: { files: [file] } })
// Wait for FileReader to process and button to be enabled
await waitFor(() => {
const importButton = screen.getByText('common.overwriteAndImport')
expect(importButton).not.toBeDisabled()
})
// Give extra time for the FileReader's queueMicrotask to complete
await new Promise(resolve => setTimeout(resolve, 10))
const importButton = screen.getByText('common.overwriteAndImport')
fireEvent.click(importButton)
@ -703,7 +707,7 @@ describe('UpdateDSLModal', () => {
await waitFor(() => {
expect(screen.getByText('1.0.0')).toBeInTheDocument()
expect(screen.getByText('2.0.0')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
})
it('should close error modal when cancel button is clicked', async () => {
@ -732,7 +736,7 @@ describe('UpdateDSLModal', () => {
// Wait for error modal
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
// Find and click cancel button in error modal - it should be the one with secondary variant
const cancelButtons = screen.getAllByText('newApp.Cancel')
@ -780,7 +784,7 @@ describe('UpdateDSLModal', () => {
// Wait for error modal
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
// Click confirm button
const confirmButton = screen.getByText('newApp.Confirm')
@ -821,7 +825,7 @@ describe('UpdateDSLModal', () => {
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
@ -863,7 +867,7 @@ describe('UpdateDSLModal', () => {
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
@ -902,7 +906,7 @@ describe('UpdateDSLModal', () => {
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
@ -944,7 +948,7 @@ describe('UpdateDSLModal', () => {
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
@ -986,7 +990,7 @@ describe('UpdateDSLModal', () => {
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
@ -1026,7 +1030,7 @@ describe('UpdateDSLModal', () => {
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
const confirmButton = screen.getByText('newApp.Confirm')
fireEvent.click(confirmButton)
@ -1062,7 +1066,7 @@ describe('UpdateDSLModal', () => {
// Should show error modal even with undefined versions
await waitFor(() => {
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
}, { timeout: 500 })
}, { timeout: 1000 })
})
it('should not call importDSLConfirm when importId is not set', async () => {