mirror of
https://github.com/langgenius/dify.git
synced 2026-04-29 06:58:05 +08:00
test: enhance dataset card and rag pipeline modal tests with additional mocks and timeout adjustments
This commit is contained in:
@ -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: () => ({
|
||||
|
||||
@ -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)
|
||||
})
|
||||
|
||||
@ -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
|
||||
// ============================================================================
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user