mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 14:19:28 +00:00
test: add comprehensive tests for TemplateCard and Details components in CreateFromPipeline feature
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,786 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import Details from './index'
|
||||
import type { PipelineTemplateByIdResponse } from '@/models/pipeline'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import type { Edge, Node, Viewport } from 'reactflow'
|
||||
|
||||
// Mock usePipelineTemplateById hook
|
||||
let mockPipelineTemplateData: PipelineTemplateByIdResponse | undefined
|
||||
let mockIsLoading = false
|
||||
|
||||
jest.mock('@/service/use-pipeline', () => ({
|
||||
usePipelineTemplateById: (params: { template_id: string; type: 'customized' | 'built-in' }, enabled: boolean) => ({
|
||||
data: enabled ? mockPipelineTemplateData : undefined,
|
||||
isLoading: mockIsLoading,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock WorkflowPreview component to avoid deep dependencies
|
||||
jest.mock('@/app/components/workflow/workflow-preview', () => ({
|
||||
__esModule: true,
|
||||
default: ({ nodes, edges, viewport, className }: {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
viewport: Viewport
|
||||
className?: string
|
||||
}) => (
|
||||
<div
|
||||
data-testid="workflow-preview"
|
||||
data-nodes-count={nodes?.length ?? 0}
|
||||
data-edges-count={edges?.length ?? 0}
|
||||
data-viewport-zoom={viewport?.zoom}
|
||||
className={className}
|
||||
>
|
||||
WorkflowPreview
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Factory function for creating mock pipeline template response
|
||||
const createMockPipelineTemplate = (
|
||||
overrides: Partial<PipelineTemplateByIdResponse> = {},
|
||||
): PipelineTemplateByIdResponse => ({
|
||||
id: 'test-template-id',
|
||||
name: 'Test Pipeline Template',
|
||||
icon_info: {
|
||||
icon_type: 'emoji',
|
||||
icon: '📙',
|
||||
icon_background: '#FFF4ED',
|
||||
icon_url: '',
|
||||
},
|
||||
description: 'Test pipeline description for testing purposes',
|
||||
chunk_structure: ChunkingMode.text,
|
||||
export_data: '{}',
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: 'node-1', type: 'custom', position: { x: 0, y: 0 }, data: {} },
|
||||
] as unknown as Node[],
|
||||
edges: [] as Edge[],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
},
|
||||
created_by: 'Test Author',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Default props factory
|
||||
const createDefaultProps = () => ({
|
||||
id: 'test-id',
|
||||
type: 'built-in' as const,
|
||||
onApplyTemplate: jest.fn(),
|
||||
onClose: jest.fn(),
|
||||
})
|
||||
|
||||
describe('Details', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPipelineTemplateData = undefined
|
||||
mockIsLoading = false
|
||||
})
|
||||
|
||||
/**
|
||||
* Loading State Tests
|
||||
* Tests for component behavior when data is loading or undefined
|
||||
*/
|
||||
describe('Loading State', () => {
|
||||
it('should render Loading component when pipelineTemplateInfo is undefined', () => {
|
||||
mockPipelineTemplateData = undefined
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<Details {...props} />)
|
||||
|
||||
// Loading component renders a spinner SVG with spin-animation class
|
||||
const spinner = container.querySelector('.spin-animation')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Loading component when data is still loading', () => {
|
||||
mockIsLoading = true
|
||||
mockPipelineTemplateData = undefined
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<Details {...props} />)
|
||||
|
||||
// Loading component renders a spinner SVG with spin-animation class
|
||||
const spinner = container.querySelector('.spin-animation')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render main content while loading', () => {
|
||||
mockPipelineTemplateData = undefined
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.queryByTestId('workflow-preview')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('datasetPipeline.operations.useTemplate')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Rendering Tests
|
||||
* Tests for correct rendering when data is available
|
||||
*/
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing when data is available', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<Details {...props} />)
|
||||
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the main container with flex layout', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<Details {...props} />)
|
||||
|
||||
const mainContainer = container.firstChild as HTMLElement
|
||||
expect(mainContainer).toHaveClass('flex')
|
||||
expect(mainContainer).toHaveClass('h-full')
|
||||
})
|
||||
|
||||
it('should render WorkflowPreview component', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass graph data to WorkflowPreview', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
graph: {
|
||||
nodes: [
|
||||
{ id: '1', type: 'custom', position: { x: 0, y: 0 }, data: {} },
|
||||
{ id: '2', type: 'custom', position: { x: 100, y: 100 }, data: {} },
|
||||
] as unknown as Node[],
|
||||
edges: [
|
||||
{ id: 'e1', source: '1', target: '2' },
|
||||
] as unknown as Edge[],
|
||||
viewport: { x: 10, y: 20, zoom: 1.5 },
|
||||
},
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const preview = screen.getByTestId('workflow-preview')
|
||||
expect(preview).toHaveAttribute('data-nodes-count', '2')
|
||||
expect(preview).toHaveAttribute('data-edges-count', '1')
|
||||
expect(preview).toHaveAttribute('data-viewport-zoom', '1.5')
|
||||
})
|
||||
|
||||
it('should render template name', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: 'My Test Pipeline' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('My Test Pipeline')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render template description', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ description: 'This is a test description' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('This is a test description')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render created_by information when available', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ created_by: 'John Doe' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
// The translation key includes the author
|
||||
expect(screen.getByText('datasetPipeline.details.createdBy')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render created_by when not available', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ created_by: '' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.queryByText(/createdBy/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "Use Template" button', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('datasetPipeline.operations.useTemplate')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const closeButton = screen.getByRole('button', { name: '' })
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render structure section title', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('datasetPipeline.details.structure')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render structure tooltip', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
// Tooltip component should be rendered
|
||||
expect(screen.getByText('datasetPipeline.details.structure')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Event Handler Tests
|
||||
* Tests for user interactions and callback functions
|
||||
*/
|
||||
describe('Event Handlers', () => {
|
||||
it('should call onApplyTemplate when "Use Template" button is clicked', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const useTemplateButton = screen.getByText('datasetPipeline.operations.useTemplate').closest('button')
|
||||
fireEvent.click(useTemplateButton!)
|
||||
|
||||
expect(props.onApplyTemplate).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onClose when close button is clicked', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<Details {...props} />)
|
||||
|
||||
// Find the close button (the one with RiCloseLine icon)
|
||||
const closeButton = container.querySelector('button.absolute.right-4')
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not call handlers on multiple clicks (each click should trigger once)', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const useTemplateButton = screen.getByText('datasetPipeline.operations.useTemplate').closest('button')
|
||||
fireEvent.click(useTemplateButton!)
|
||||
fireEvent.click(useTemplateButton!)
|
||||
fireEvent.click(useTemplateButton!)
|
||||
|
||||
expect(props.onApplyTemplate).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Props Variations Tests
|
||||
* Tests for different prop combinations
|
||||
*/
|
||||
describe('Props Variations', () => {
|
||||
it('should handle built-in type', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = { ...createDefaultProps(), type: 'built-in' as const }
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle customized type', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = { ...createDefaultProps(), type: 'customized' as const }
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle different template IDs', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = { ...createDefaultProps(), id: 'unique-template-123' }
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* App Icon Memoization Tests
|
||||
* Tests for the useMemo logic that computes appIcon
|
||||
*/
|
||||
describe('App Icon Memoization', () => {
|
||||
it('should use default emoji icon when pipelineTemplateInfo is undefined', () => {
|
||||
mockPipelineTemplateData = undefined
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
// Loading state - no AppIcon rendered
|
||||
expect(screen.queryByTestId('workflow-preview')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle emoji icon type', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
icon_info: {
|
||||
icon_type: 'emoji',
|
||||
icon: '🚀',
|
||||
icon_background: '#E6F4FF',
|
||||
icon_url: '',
|
||||
},
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
// AppIcon should be rendered with emoji
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle image icon type', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
icon_info: {
|
||||
icon_type: 'image',
|
||||
icon: 'file-id-123',
|
||||
icon_background: '',
|
||||
icon_url: 'https://example.com/image.png',
|
||||
},
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle image icon type with empty url and icon (fallback branch)', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
icon_info: {
|
||||
icon_type: 'image',
|
||||
icon: '', // empty string - triggers || '' fallback
|
||||
icon_background: '',
|
||||
icon_url: '', // empty string - triggers || '' fallback
|
||||
},
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
// Component should still render without errors
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle missing icon properties gracefully', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
icon_info: {
|
||||
icon_type: 'emoji',
|
||||
icon: '',
|
||||
icon_background: '',
|
||||
icon_url: '',
|
||||
},
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
expect(() => render(<Details {...props} />)).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Chunk Structure Tests
|
||||
* Tests for different chunk_structure values and ChunkStructureCard rendering
|
||||
*/
|
||||
describe('Chunk Structure', () => {
|
||||
it('should render ChunkStructureCard for text chunk structure', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
chunk_structure: ChunkingMode.text,
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
// ChunkStructureCard should be rendered
|
||||
expect(screen.getByText('datasetPipeline.details.structure')).toBeInTheDocument()
|
||||
// General option title
|
||||
expect(screen.getByText('General')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ChunkStructureCard for parentChild chunk structure', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
chunk_structure: ChunkingMode.parentChild,
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('Parent-Child')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render ChunkStructureCard for qa chunk structure', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
chunk_structure: ChunkingMode.qa,
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('Q&A')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Edge Cases Tests
|
||||
* Tests for boundary conditions and unusual inputs
|
||||
*/
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty name', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: '' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
expect(() => render(<Details {...props} />)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle empty description', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ description: '' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long name', () => {
|
||||
const longName = 'A'.repeat(200)
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: longName })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const nameElement = screen.getByText(longName)
|
||||
expect(nameElement).toBeInTheDocument()
|
||||
expect(nameElement).toHaveClass('truncate')
|
||||
})
|
||||
|
||||
it('should handle very long description', () => {
|
||||
const longDesc = 'B'.repeat(1000)
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ description: longDesc })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText(longDesc)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle special characters in name', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
name: 'Test <>&"\'Pipeline @#$%^&*()',
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('Test <>&"\'Pipeline @#$%^&*()')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle unicode characters', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
name: '测试管道 🚀 テスト',
|
||||
description: '这是一个测试描述 日本語テスト',
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('测试管道 🚀 テスト')).toBeInTheDocument()
|
||||
expect(screen.getByText('这是一个测试描述 日本語テスト')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle empty graph nodes and edges', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
},
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const preview = screen.getByTestId('workflow-preview')
|
||||
expect(preview).toHaveAttribute('data-nodes-count', '0')
|
||||
expect(preview).toHaveAttribute('data-edges-count', '0')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Component Memoization Tests
|
||||
* Tests for React.memo behavior
|
||||
*/
|
||||
describe('Component Memoization', () => {
|
||||
it('should render correctly after rerender with same props', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('Test Pipeline Template')).toBeInTheDocument()
|
||||
|
||||
rerender(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('Test Pipeline Template')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update when id prop changes', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: 'First Template' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('First Template')).toBeInTheDocument()
|
||||
|
||||
// Change the id prop which should trigger a rerender
|
||||
// Update mock data for the new id
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: 'Second Template' })
|
||||
rerender(<Details {...props} id="new-id" />)
|
||||
|
||||
expect(screen.getByText('Second Template')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle callback reference changes', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<Details {...props} />)
|
||||
|
||||
const newOnApplyTemplate = jest.fn()
|
||||
rerender(<Details {...props} onApplyTemplate={newOnApplyTemplate} />)
|
||||
|
||||
const useTemplateButton = screen.getByText('datasetPipeline.operations.useTemplate').closest('button')
|
||||
fireEvent.click(useTemplateButton!)
|
||||
|
||||
expect(newOnApplyTemplate).toHaveBeenCalledTimes(1)
|
||||
expect(props.onApplyTemplate).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Component Structure Tests
|
||||
* Tests for DOM structure and layout
|
||||
*/
|
||||
describe('Component Structure', () => {
|
||||
it('should have left panel for workflow preview', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<Details {...props} />)
|
||||
|
||||
const leftPanel = container.querySelector('.grow.items-center.justify-center')
|
||||
expect(leftPanel).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have right panel with fixed width', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<Details {...props} />)
|
||||
|
||||
const rightPanel = container.querySelector('.w-\\[360px\\]')
|
||||
expect(rightPanel).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have primary button variant for Use Template', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const button = screen.getByText('datasetPipeline.operations.useTemplate').closest('button')
|
||||
// Button should have primary styling
|
||||
expect(button).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have title attribute for truncation tooltip', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: 'My Pipeline Name' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const nameElement = screen.getByText('My Pipeline Name')
|
||||
expect(nameElement).toHaveAttribute('title', 'My Pipeline Name')
|
||||
})
|
||||
|
||||
it('should have title attribute on created_by for truncation', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ created_by: 'Author Name' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const createdByElement = screen.getByText('datasetPipeline.details.createdBy')
|
||||
expect(createdByElement).toHaveAttribute('title', 'Author Name')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Component Lifecycle Tests
|
||||
* Tests for mount/unmount behavior
|
||||
*/
|
||||
describe('Component Lifecycle', () => {
|
||||
it('should mount without errors', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
expect(() => render(<Details {...props} />)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should unmount without errors', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { unmount } = render(<Details {...props} />)
|
||||
|
||||
expect(() => unmount()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle rapid mount/unmount cycles', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const { unmount } = render(<Details {...props} />)
|
||||
unmount()
|
||||
}
|
||||
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
it('should transition from loading to loaded state', () => {
|
||||
mockPipelineTemplateData = undefined
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender, container } = render(<Details {...props} />)
|
||||
|
||||
// Loading component renders a spinner SVG with spin-animation class
|
||||
const spinner = container.querySelector('.spin-animation')
|
||||
expect(spinner).toBeInTheDocument()
|
||||
|
||||
// Simulate data loaded - need to change props to trigger rerender with React.memo
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
rerender(<Details {...props} id="loaded-id" />)
|
||||
|
||||
expect(container.querySelector('.spin-animation')).not.toBeInTheDocument()
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Styling Tests
|
||||
* Tests for CSS classes and visual styling
|
||||
*/
|
||||
describe('Styling', () => {
|
||||
it('should apply overflow-hidden rounded-2xl to WorkflowPreview container', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const preview = screen.getByTestId('workflow-preview')
|
||||
expect(preview).toHaveClass('overflow-hidden')
|
||||
expect(preview).toHaveClass('rounded-2xl')
|
||||
})
|
||||
|
||||
it('should apply correct typography classes to template name', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const nameElement = screen.getByText('Test Pipeline Template')
|
||||
expect(nameElement).toHaveClass('system-md-semibold')
|
||||
expect(nameElement).toHaveClass('text-text-secondary')
|
||||
})
|
||||
|
||||
it('should apply correct styling to description', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const description = screen.getByText('Test pipeline description for testing purposes')
|
||||
expect(description).toHaveClass('system-sm-regular')
|
||||
expect(description).toHaveClass('text-text-secondary')
|
||||
})
|
||||
|
||||
it('should apply correct styling to structure title', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
const structureTitle = screen.getByText('datasetPipeline.details.structure')
|
||||
expect(structureTitle).toHaveClass('system-sm-semibold-uppercase')
|
||||
expect(structureTitle).toHaveClass('text-text-secondary')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* API Hook Integration Tests
|
||||
* Tests for usePipelineTemplateById hook behavior
|
||||
*/
|
||||
describe('API Hook Integration', () => {
|
||||
it('should pass correct params to usePipelineTemplateById for built-in type', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = { ...createDefaultProps(), id: 'test-id-123', type: 'built-in' as const }
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
// The hook should be called with the correct parameters
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass correct params to usePipelineTemplateById for customized type', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate()
|
||||
const props = { ...createDefaultProps(), id: 'custom-id-456', type: 'customized' as const }
|
||||
|
||||
render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByTestId('workflow-preview')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle data refetch on id change', () => {
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: 'First Template' })
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<Details {...props} />)
|
||||
|
||||
expect(screen.getByText('First Template')).toBeInTheDocument()
|
||||
|
||||
// Change id and update mock data
|
||||
mockPipelineTemplateData = createMockPipelineTemplate({ name: 'Second Template' })
|
||||
rerender(<Details {...props} id="new-id" />)
|
||||
|
||||
expect(screen.getByText('Second Template')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,965 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import TemplateCard from './index'
|
||||
import type { PipelineTemplate, PipelineTemplateByIdResponse } from '@/models/pipeline'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
|
||||
// Mock Next.js router
|
||||
const mockPush = jest.fn()
|
||||
jest.mock('next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
}),
|
||||
}))
|
||||
|
||||
let mockCreateDataset: jest.Mock
|
||||
let mockDeleteTemplate: jest.Mock
|
||||
let mockExportTemplateDSL: jest.Mock
|
||||
let mockInvalidCustomizedTemplateList: jest.Mock
|
||||
let mockInvalidDatasetList: jest.Mock
|
||||
let mockHandleCheckPluginDependencies: jest.Mock
|
||||
let mockIsExporting = false
|
||||
|
||||
// Mock service hooks
|
||||
let mockPipelineTemplateByIdData: PipelineTemplateByIdResponse | undefined
|
||||
let mockRefetch: jest.Mock
|
||||
|
||||
jest.mock('@/service/use-pipeline', () => ({
|
||||
usePipelineTemplateById: () => ({
|
||||
data: mockPipelineTemplateByIdData,
|
||||
refetch: mockRefetch,
|
||||
}),
|
||||
useDeleteTemplate: () => ({
|
||||
mutateAsync: mockDeleteTemplate,
|
||||
}),
|
||||
useExportTemplateDSL: () => ({
|
||||
mutateAsync: mockExportTemplateDSL,
|
||||
isPending: mockIsExporting,
|
||||
}),
|
||||
useInvalidCustomizedTemplateList: () => mockInvalidCustomizedTemplateList,
|
||||
}))
|
||||
|
||||
jest.mock('@/service/knowledge/use-create-dataset', () => ({
|
||||
useCreatePipelineDatasetFromCustomized: () => ({
|
||||
mutateAsync: mockCreateDataset,
|
||||
}),
|
||||
}))
|
||||
|
||||
jest.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useInvalidDatasetList: () => mockInvalidDatasetList,
|
||||
}))
|
||||
|
||||
jest.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
|
||||
usePluginDependencies: () => ({
|
||||
handleCheckPluginDependencies: mockHandleCheckPluginDependencies,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock downloadFile
|
||||
const mockDownloadFile = jest.fn()
|
||||
jest.mock('@/utils/format', () => ({
|
||||
downloadFile: (params: { data: Blob; fileName: string }) => mockDownloadFile(params),
|
||||
}))
|
||||
|
||||
// Mock trackEvent
|
||||
const mockTrackEvent = jest.fn()
|
||||
jest.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: (name: string, params: Record<string, unknown>) => mockTrackEvent(name, params),
|
||||
}))
|
||||
|
||||
// Mock child components to simplify testing
|
||||
jest.mock('./content', () => ({
|
||||
__esModule: true,
|
||||
default: ({ name, description, iconInfo, chunkStructure }: {
|
||||
name: string
|
||||
description: string
|
||||
iconInfo: { icon_type: string }
|
||||
chunkStructure: string
|
||||
}) => (
|
||||
<div data-testid="content">
|
||||
<span data-testid="content-name">{name}</span>
|
||||
<span data-testid="content-description">{description}</span>
|
||||
<span data-testid="content-icon-type">{iconInfo.icon_type}</span>
|
||||
<span data-testid="content-chunk-structure">{chunkStructure}</span>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('./actions', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onApplyTemplate,
|
||||
handleShowTemplateDetails,
|
||||
showMoreOperations,
|
||||
openEditModal,
|
||||
handleExportDSL,
|
||||
handleDelete,
|
||||
}: {
|
||||
onApplyTemplate: () => void
|
||||
handleShowTemplateDetails: () => void
|
||||
showMoreOperations: boolean
|
||||
openEditModal: () => void
|
||||
handleExportDSL: () => void
|
||||
handleDelete: () => void
|
||||
}) => (
|
||||
<div data-testid="actions" data-show-more={showMoreOperations}>
|
||||
<button data-testid="apply-template-btn" onClick={onApplyTemplate}>Apply</button>
|
||||
<button data-testid="show-details-btn" onClick={handleShowTemplateDetails}>Details</button>
|
||||
<button data-testid="edit-modal-btn" onClick={openEditModal}>Edit</button>
|
||||
<button data-testid="export-dsl-btn" onClick={handleExportDSL}>Export</button>
|
||||
<button data-testid="delete-btn" onClick={handleDelete}>Delete</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('./details', () => ({
|
||||
__esModule: true,
|
||||
default: ({ id, type, onClose, onApplyTemplate }: {
|
||||
id: string
|
||||
type: string
|
||||
onClose: () => void
|
||||
onApplyTemplate: () => void
|
||||
}) => (
|
||||
<div data-testid="details-modal">
|
||||
<span data-testid="details-id">{id}</span>
|
||||
<span data-testid="details-type">{type}</span>
|
||||
<button data-testid="details-close-btn" onClick={onClose}>Close</button>
|
||||
<button data-testid="details-apply-btn" onClick={onApplyTemplate}>Apply</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
jest.mock('./edit-pipeline-info', () => ({
|
||||
__esModule: true,
|
||||
default: ({ pipeline, onClose }: {
|
||||
pipeline: PipelineTemplate
|
||||
onClose: () => void
|
||||
}) => (
|
||||
<div data-testid="edit-pipeline-modal">
|
||||
<span data-testid="edit-pipeline-id">{pipeline.id}</span>
|
||||
<button data-testid="edit-close-btn" onClick={onClose}>Close</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Factory function for creating mock pipeline template
|
||||
const createMockPipeline = (overrides: Partial<PipelineTemplate> = {}): PipelineTemplate => ({
|
||||
id: 'test-pipeline-id',
|
||||
name: 'Test Pipeline',
|
||||
description: 'Test pipeline description',
|
||||
icon: {
|
||||
icon_type: 'emoji',
|
||||
icon: '📙',
|
||||
icon_background: '#FFF4ED',
|
||||
icon_url: '',
|
||||
},
|
||||
position: 1,
|
||||
chunk_structure: ChunkingMode.text,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Factory function for creating mock pipeline template by id response
|
||||
const createMockPipelineByIdResponse = (
|
||||
overrides: Partial<PipelineTemplateByIdResponse> = {},
|
||||
): PipelineTemplateByIdResponse => ({
|
||||
id: 'test-pipeline-id',
|
||||
name: 'Test Pipeline',
|
||||
description: 'Test pipeline description',
|
||||
icon_info: {
|
||||
icon_type: 'emoji',
|
||||
icon: '📙',
|
||||
icon_background: '#FFF4ED',
|
||||
icon_url: '',
|
||||
},
|
||||
chunk_structure: ChunkingMode.text,
|
||||
export_data: 'yaml_content_here',
|
||||
graph: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
},
|
||||
created_by: 'Test Author',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// Default props factory
|
||||
const createDefaultProps = () => ({
|
||||
pipeline: createMockPipeline(),
|
||||
type: 'built-in' as const,
|
||||
showMoreOperations: true,
|
||||
})
|
||||
|
||||
describe('TemplateCard', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockPipelineTemplateByIdData = undefined
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn()
|
||||
mockDeleteTemplate = jest.fn()
|
||||
mockExportTemplateDSL = jest.fn()
|
||||
mockInvalidCustomizedTemplateList = jest.fn()
|
||||
mockInvalidDatasetList = jest.fn()
|
||||
mockHandleCheckPluginDependencies = jest.fn()
|
||||
mockIsExporting = false
|
||||
})
|
||||
|
||||
/**
|
||||
* Rendering Tests
|
||||
* Tests for basic component rendering
|
||||
*/
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('actions')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Content component with correct props', () => {
|
||||
const pipeline = createMockPipeline({
|
||||
name: 'My Pipeline',
|
||||
description: 'My description',
|
||||
chunk_structure: ChunkingMode.qa,
|
||||
})
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent('My Pipeline')
|
||||
expect(screen.getByTestId('content-description')).toHaveTextContent('My description')
|
||||
expect(screen.getByTestId('content-chunk-structure')).toHaveTextContent(ChunkingMode.qa)
|
||||
})
|
||||
|
||||
it('should render Actions component with showMoreOperations=true by default', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
const actions = screen.getByTestId('actions')
|
||||
expect(actions).toHaveAttribute('data-show-more', 'true')
|
||||
})
|
||||
|
||||
it('should render Actions component with showMoreOperations=false when specified', () => {
|
||||
const props = { ...createDefaultProps(), showMoreOperations: false }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
const actions = screen.getByTestId('actions')
|
||||
expect(actions).toHaveAttribute('data-show-more', 'false')
|
||||
})
|
||||
|
||||
it('should have correct container styling', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { container } = render(<TemplateCard {...props} />)
|
||||
|
||||
const card = container.firstChild as HTMLElement
|
||||
expect(card).toHaveClass('group')
|
||||
expect(card).toHaveClass('relative')
|
||||
expect(card).toHaveClass('flex')
|
||||
expect(card).toHaveClass('h-[132px]')
|
||||
expect(card).toHaveClass('cursor-pointer')
|
||||
expect(card).toHaveClass('rounded-xl')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Props Variations Tests
|
||||
* Tests for different prop combinations
|
||||
*/
|
||||
describe('Props Variations', () => {
|
||||
it('should handle built-in type', () => {
|
||||
const props = { ...createDefaultProps(), type: 'built-in' as const }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle customized type', () => {
|
||||
const props = { ...createDefaultProps(), type: 'customized' as const }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle different pipeline data', () => {
|
||||
const pipeline = createMockPipeline({
|
||||
id: 'unique-id-123',
|
||||
name: 'Unique Pipeline',
|
||||
description: 'Unique description',
|
||||
chunk_structure: ChunkingMode.parentChild,
|
||||
})
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent('Unique Pipeline')
|
||||
expect(screen.getByTestId('content-chunk-structure')).toHaveTextContent(ChunkingMode.parentChild)
|
||||
})
|
||||
|
||||
it('should handle image icon type', () => {
|
||||
const pipeline = createMockPipeline({
|
||||
icon: {
|
||||
icon_type: 'image',
|
||||
icon: 'file-id',
|
||||
icon_background: '',
|
||||
icon_url: 'https://example.com/image.png',
|
||||
},
|
||||
})
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-icon-type')).toHaveTextContent('image')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* State Management Tests
|
||||
* Tests for modal state (showEditModal, showDeleteConfirm, showDetailModal)
|
||||
*/
|
||||
describe('State Management', () => {
|
||||
it('should not show edit modal initially', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.queryByTestId('edit-pipeline-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show edit modal when openEditModal is called', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-modal-btn'))
|
||||
|
||||
expect(screen.getByTestId('edit-pipeline-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close edit modal when onClose is called', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-modal-btn'))
|
||||
expect(screen.getByTestId('edit-pipeline-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-close-btn'))
|
||||
expect(screen.queryByTestId('edit-pipeline-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show delete confirm initially', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.queryByText('datasetPipeline.deletePipeline.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show delete confirm when handleDelete is called', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('delete-btn'))
|
||||
|
||||
expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show details modal initially', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.queryByTestId('details-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show details modal when handleShowTemplateDetails is called', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-details-btn'))
|
||||
|
||||
expect(screen.getByTestId('details-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close details modal when onClose is called', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-details-btn'))
|
||||
expect(screen.getByTestId('details-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('details-close-btn'))
|
||||
expect(screen.queryByTestId('details-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pass correct props to details modal', () => {
|
||||
const pipeline = createMockPipeline({ id: 'detail-test-id' })
|
||||
const props = { ...createDefaultProps(), pipeline, type: 'customized' as const }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-details-btn'))
|
||||
|
||||
expect(screen.getByTestId('details-id')).toHaveTextContent('detail-test-id')
|
||||
expect(screen.getByTestId('details-type')).toHaveTextContent('customized')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Event Handlers Tests
|
||||
* Tests for callback functions and user interactions
|
||||
*/
|
||||
describe('Event Handlers', () => {
|
||||
describe('handleUseTemplate', () => {
|
||||
it('should call getPipelineTemplateInfo when apply template is clicked', async () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call createDataset when pipelineTemplateInfo is not available', async () => {
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: null })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// createDataset should not be called when pipelineTemplateInfo is null
|
||||
expect(mockCreateDataset).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call createDataset with correct yaml_content', async () => {
|
||||
const pipelineResponse = createMockPipelineByIdResponse({ export_data: 'test-yaml-content' })
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: pipelineResponse })
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateDataset).toHaveBeenCalledWith(
|
||||
{ yaml_content: 'test-yaml-content' },
|
||||
expect.any(Object),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('should invalidate list, check plugin dependencies, and navigate on success', async () => {
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn().mockImplementation((_req, options) => {
|
||||
options.onSuccess({ dataset_id: 'new-dataset-id', pipeline_id: 'new-pipeline-id' })
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInvalidDatasetList).toHaveBeenCalled()
|
||||
expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('new-pipeline-id', true)
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/new-dataset-id/pipeline')
|
||||
})
|
||||
})
|
||||
|
||||
it('should track event on successful dataset creation', async () => {
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn().mockImplementation((_req, options) => {
|
||||
options.onSuccess({ dataset_id: 'new-dataset-id', pipeline_id: 'new-pipeline-id' })
|
||||
})
|
||||
const pipeline = createMockPipeline({ id: 'track-test-id', name: 'Track Test Pipeline' })
|
||||
const props = { ...createDefaultProps(), pipeline, type: 'customized' as const }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockTrackEvent).toHaveBeenCalledWith('create_datasets_with_pipeline', {
|
||||
template_name: 'Track Test Pipeline',
|
||||
template_id: 'track-test-id',
|
||||
template_type: 'customized',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call handleCheckPluginDependencies when pipeline_id is not present', async () => {
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn().mockImplementation((_req, options) => {
|
||||
options.onSuccess({ dataset_id: 'new-dataset-id', pipeline_id: null })
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHandleCheckPluginDependencies).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onError callback when createDataset fails', async () => {
|
||||
const onErrorSpy = jest.fn()
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn().mockImplementation((_req, options) => {
|
||||
onErrorSpy()
|
||||
options.onError()
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateDataset).toHaveBeenCalled()
|
||||
expect(onErrorSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Should not navigate on error
|
||||
expect(mockPush).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleExportDSL', () => {
|
||||
it('should call exportPipelineDSL with pipeline id', async () => {
|
||||
const pipeline = createMockPipeline({ id: 'export-test-id' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('export-dsl-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockExportTemplateDSL).toHaveBeenCalledWith('export-test-id', expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
it('should not call exportPipelineDSL when already exporting', async () => {
|
||||
mockIsExporting = true
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('export-dsl-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockExportTemplateDSL).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should download file on export success', async () => {
|
||||
mockExportTemplateDSL = jest.fn().mockImplementation((_id, options) => {
|
||||
options.onSuccess({ data: 'exported-yaml-content' })
|
||||
})
|
||||
const pipeline = createMockPipeline({ name: 'Export Pipeline' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('export-dsl-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDownloadFile).toHaveBeenCalledWith({
|
||||
data: expect.any(Blob),
|
||||
fileName: 'Export Pipeline.pipeline',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onError callback on export failure', async () => {
|
||||
const onErrorSpy = jest.fn()
|
||||
mockExportTemplateDSL = jest.fn().mockImplementation((_id, options) => {
|
||||
onErrorSpy()
|
||||
options.onError()
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('export-dsl-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockExportTemplateDSL).toHaveBeenCalled()
|
||||
expect(onErrorSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Should not download file on error
|
||||
expect(mockDownloadFile).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleDelete', () => {
|
||||
it('should call deletePipeline on confirm', async () => {
|
||||
mockDeleteTemplate = jest.fn().mockImplementation((_id, options) => {
|
||||
options.onSuccess()
|
||||
})
|
||||
const pipeline = createMockPipeline({ id: 'delete-test-id' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('delete-btn'))
|
||||
expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument()
|
||||
|
||||
// Find and click confirm button
|
||||
const confirmButton = screen.getByText('common.operation.confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteTemplate).toHaveBeenCalledWith('delete-test-id', expect.any(Object))
|
||||
})
|
||||
})
|
||||
|
||||
it('should invalidate customized template list and close confirm on success', async () => {
|
||||
mockDeleteTemplate = jest.fn().mockImplementation((_id, options) => {
|
||||
options.onSuccess()
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('delete-btn'))
|
||||
const confirmButton = screen.getByText('common.operation.confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockInvalidCustomizedTemplateList).toHaveBeenCalled()
|
||||
expect(screen.queryByText('datasetPipeline.deletePipeline.title')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should close delete confirm on cancel', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('delete-btn'))
|
||||
expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument()
|
||||
|
||||
const cancelButton = screen.getByText('common.operation.cancel')
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(screen.queryByText('datasetPipeline.deletePipeline.title')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Callback Stability Tests
|
||||
* Tests for useCallback memoization
|
||||
*/
|
||||
describe('Callback Stability', () => {
|
||||
it('should maintain stable handleShowTemplateDetails reference', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-details-btn'))
|
||||
expect(screen.getByTestId('details-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('details-close-btn'))
|
||||
rerender(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-details-btn'))
|
||||
expect(screen.getByTestId('details-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should maintain stable openEditModal reference', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-modal-btn'))
|
||||
expect(screen.getByTestId('edit-pipeline-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-close-btn'))
|
||||
rerender(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-modal-btn'))
|
||||
expect(screen.getByTestId('edit-pipeline-modal')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Component Memoization Tests
|
||||
* Tests for React.memo behavior
|
||||
*/
|
||||
describe('Component Memoization', () => {
|
||||
it('should render correctly after rerender with same props', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
|
||||
rerender(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update when pipeline prop changes', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent('Test Pipeline')
|
||||
|
||||
const newPipeline = createMockPipeline({ name: 'Updated Pipeline' })
|
||||
rerender(<TemplateCard {...props} pipeline={newPipeline} />)
|
||||
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent('Updated Pipeline')
|
||||
})
|
||||
|
||||
it('should update when type prop changes', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
|
||||
rerender(<TemplateCard {...props} type="customized" />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update when showMoreOperations prop changes', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { rerender } = render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('actions')).toHaveAttribute('data-show-more', 'true')
|
||||
|
||||
rerender(<TemplateCard {...props} showMoreOperations={false} />)
|
||||
|
||||
expect(screen.getByTestId('actions')).toHaveAttribute('data-show-more', 'false')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Edge Cases Tests
|
||||
* Tests for boundary conditions and error handling
|
||||
*/
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty pipeline name', () => {
|
||||
const pipeline = createMockPipeline({ name: '' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
expect(() => render(<TemplateCard {...props} />)).not.toThrow()
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle empty pipeline description', () => {
|
||||
const pipeline = createMockPipeline({ description: '' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
expect(() => render(<TemplateCard {...props} />)).not.toThrow()
|
||||
expect(screen.getByTestId('content-description')).toHaveTextContent('')
|
||||
})
|
||||
|
||||
it('should handle very long pipeline name', () => {
|
||||
const longName = 'A'.repeat(200)
|
||||
const pipeline = createMockPipeline({ name: longName })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent(longName)
|
||||
})
|
||||
|
||||
it('should handle special characters in name', () => {
|
||||
const pipeline = createMockPipeline({ name: 'Test <>&"\'Pipeline @#$%' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent('Test <>&"\'Pipeline @#$%')
|
||||
})
|
||||
|
||||
it('should handle unicode characters', () => {
|
||||
const pipeline = createMockPipeline({ name: '测试管道 🚀 テスト' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-name')).toHaveTextContent('测试管道 🚀 テスト')
|
||||
})
|
||||
|
||||
it('should handle all chunk structure types', () => {
|
||||
const chunkModes = [ChunkingMode.text, ChunkingMode.parentChild, ChunkingMode.qa]
|
||||
|
||||
chunkModes.forEach((mode) => {
|
||||
const pipeline = createMockPipeline({ chunk_structure: mode })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
const { unmount } = render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content-chunk-structure')).toHaveTextContent(mode)
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Component Lifecycle Tests
|
||||
* Tests for mount/unmount behavior
|
||||
*/
|
||||
describe('Component Lifecycle', () => {
|
||||
it('should mount without errors', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
expect(() => render(<TemplateCard {...props} />)).not.toThrow()
|
||||
})
|
||||
|
||||
it('should unmount without errors', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
const { unmount } = render(<TemplateCard {...props} />)
|
||||
|
||||
expect(() => unmount()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle rapid mount/unmount cycles', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const { unmount } = render(<TemplateCard {...props} />)
|
||||
unmount()
|
||||
}
|
||||
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Modal Integration Tests
|
||||
* Tests for modal interactions and nested callbacks
|
||||
*/
|
||||
describe('Modal Integration', () => {
|
||||
it('should pass correct pipeline to edit modal', () => {
|
||||
const pipeline = createMockPipeline({ id: 'modal-test-id' })
|
||||
const props = { ...createDefaultProps(), pipeline }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('edit-modal-btn'))
|
||||
|
||||
expect(screen.getByTestId('edit-pipeline-id')).toHaveTextContent('modal-test-id')
|
||||
})
|
||||
|
||||
it('should be able to apply template from details modal', async () => {
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn().mockImplementation((_req, options) => {
|
||||
options.onSuccess({ dataset_id: 'new-id', pipeline_id: 'new-pipeline' })
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('show-details-btn'))
|
||||
fireEvent.click(screen.getByTestId('details-apply-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
expect(mockCreateDataset).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle multiple modals sequentially', () => {
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
// Open edit modal
|
||||
fireEvent.click(screen.getByTestId('edit-modal-btn'))
|
||||
expect(screen.getByTestId('edit-pipeline-modal')).toBeInTheDocument()
|
||||
|
||||
// Close edit modal
|
||||
fireEvent.click(screen.getByTestId('edit-close-btn'))
|
||||
expect(screen.queryByTestId('edit-pipeline-modal')).not.toBeInTheDocument()
|
||||
|
||||
// Open details modal
|
||||
fireEvent.click(screen.getByTestId('show-details-btn'))
|
||||
expect(screen.getByTestId('details-modal')).toBeInTheDocument()
|
||||
|
||||
// Close details modal
|
||||
fireEvent.click(screen.getByTestId('details-close-btn'))
|
||||
expect(screen.queryByTestId('details-modal')).not.toBeInTheDocument()
|
||||
|
||||
// Open delete confirm
|
||||
fireEvent.click(screen.getByTestId('delete-btn'))
|
||||
expect(screen.getByText('datasetPipeline.deletePipeline.title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* API Integration Tests
|
||||
* Tests for service hook interactions
|
||||
*/
|
||||
describe('API Integration', () => {
|
||||
it('should initialize hooks with correct parameters', () => {
|
||||
const pipeline = createMockPipeline({ id: 'hook-test-id' })
|
||||
const props = { ...createDefaultProps(), pipeline, type: 'customized' as const }
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
expect(screen.getByTestId('content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle async operations correctly', async () => {
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn().mockImplementation(async (_req, options) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
options.onSuccess({ dataset_id: 'async-test-id', pipeline_id: 'async-pipeline' })
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/async-test-id/pipeline')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle concurrent API calls gracefully', async () => {
|
||||
mockRefetch = jest.fn().mockResolvedValue({ data: createMockPipelineByIdResponse() })
|
||||
mockCreateDataset = jest.fn().mockImplementation((_req, options) => {
|
||||
options.onSuccess({ dataset_id: 'concurrent-id', pipeline_id: 'concurrent-pipeline' })
|
||||
})
|
||||
const props = createDefaultProps()
|
||||
|
||||
render(<TemplateCard {...props} />)
|
||||
|
||||
// Trigger multiple clicks
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
fireEvent.click(screen.getByTestId('apply-template-btn'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRefetch).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user