Compare commits

...

5 Commits

6 changed files with 4635 additions and 0 deletions

View File

@@ -0,0 +1,564 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import Tab from './index'
// Define enum locally to avoid importing the whole module
enum CreateFromDSLModalTab {
FROM_FILE = 'from-file',
FROM_URL = 'from-url',
}
// Mock the create-from-dsl-modal module to export the enum
jest.mock('@/app/components/app/create-from-dsl-modal', () => ({
CreateFromDSLModalTab: {
FROM_FILE: 'from-file',
FROM_URL: 'from-url',
},
}))
// Mock react-i18next
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
describe('Tab', () => {
beforeEach(() => {
jest.clearAllMocks()
})
// Tests for basic rendering
describe('Rendering', () => {
it('should render without crashing', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
expect(screen.getByText('app.importFromDSLFile')).toBeInTheDocument()
expect(screen.getByText('app.importFromDSLUrl')).toBeInTheDocument()
})
it('should render two tab items', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
// Should have 2 clickable tab items
const tabItems = container.querySelectorAll('.cursor-pointer')
expect(tabItems.length).toBe(2)
})
it('should render with correct container styling', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const tabContainer = container.firstChild as HTMLElement
expect(tabContainer).toHaveClass('flex')
expect(tabContainer).toHaveClass('h-9')
expect(tabContainer).toHaveClass('items-center')
expect(tabContainer).toHaveClass('gap-x-6')
expect(tabContainer).toHaveClass('border-b')
expect(tabContainer).toHaveClass('border-divider-subtle')
expect(tabContainer).toHaveClass('px-6')
})
it('should render tab labels with translation keys', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
expect(screen.getByText('app.importFromDSLFile')).toBeInTheDocument()
expect(screen.getByText('app.importFromDSLUrl')).toBeInTheDocument()
})
})
// Tests for active tab indication
describe('Active Tab Indication', () => {
it('should show FROM_FILE tab as active when currentTab is FROM_FILE', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
// getByText returns the Item element directly (text is inside it)
const fileTab = screen.getByText('app.importFromDSLFile')
const urlTab = screen.getByText('app.importFromDSLUrl')
// Active tab should have text-text-primary class
expect(fileTab).toHaveClass('text-text-primary')
// Inactive tab should have text-text-tertiary class
expect(urlTab).toHaveClass('text-text-tertiary')
expect(urlTab).not.toHaveClass('text-text-primary')
})
it('should show FROM_URL tab as active when currentTab is FROM_URL', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_URL}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
const urlTab = screen.getByText('app.importFromDSLUrl')
// Inactive tab should have text-text-tertiary class
expect(fileTab).toHaveClass('text-text-tertiary')
expect(fileTab).not.toHaveClass('text-text-primary')
// Active tab should have text-text-primary class
expect(urlTab).toHaveClass('text-text-primary')
})
it('should render active indicator bar for active tab', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
// Active tab should have the indicator bar
const indicatorBars = container.querySelectorAll('.bg-util-colors-blue-brand-blue-brand-600')
expect(indicatorBars.length).toBe(1)
})
it('should render active indicator bar for URL tab when active', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_URL}
setCurrentTab={setCurrentTab}
/>,
)
// Should have one indicator bar
const indicatorBars = container.querySelectorAll('.bg-util-colors-blue-brand-blue-brand-600')
expect(indicatorBars.length).toBe(1)
// The indicator should be in the URL tab
const urlTab = screen.getByText('app.importFromDSLUrl')
expect(urlTab.querySelector('.bg-util-colors-blue-brand-blue-brand-600')).toBeInTheDocument()
})
it('should not render indicator bar for inactive tab', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
// The URL tab (inactive) should not have an indicator bar
const urlTab = screen.getByText('app.importFromDSLUrl')
expect(urlTab.querySelector('.bg-util-colors-blue-brand-blue-brand-600')).not.toBeInTheDocument()
})
})
// Tests for user interactions
describe('User Interactions', () => {
it('should call setCurrentTab with FROM_FILE when file tab is clicked', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_URL}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
fireEvent.click(fileTab)
expect(setCurrentTab).toHaveBeenCalledTimes(1)
// .bind() passes tab.key as first arg, event as second
expect(setCurrentTab).toHaveBeenCalledWith(CreateFromDSLModalTab.FROM_FILE, expect.anything())
})
it('should call setCurrentTab with FROM_URL when url tab is clicked', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const urlTab = screen.getByText('app.importFromDSLUrl')
fireEvent.click(urlTab)
expect(setCurrentTab).toHaveBeenCalledTimes(1)
expect(setCurrentTab).toHaveBeenCalledWith(CreateFromDSLModalTab.FROM_URL, expect.anything())
})
it('should call setCurrentTab when clicking already active tab', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
fireEvent.click(fileTab)
// Should still call setCurrentTab even for active tab
expect(setCurrentTab).toHaveBeenCalledTimes(1)
expect(setCurrentTab).toHaveBeenCalledWith(CreateFromDSLModalTab.FROM_FILE, expect.anything())
})
it('should handle multiple tab clicks', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
const urlTab = screen.getByText('app.importFromDSLUrl')
fireEvent.click(urlTab)
fireEvent.click(fileTab)
fireEvent.click(urlTab)
expect(setCurrentTab).toHaveBeenCalledTimes(3)
expect(setCurrentTab).toHaveBeenNthCalledWith(1, CreateFromDSLModalTab.FROM_URL, expect.anything())
expect(setCurrentTab).toHaveBeenNthCalledWith(2, CreateFromDSLModalTab.FROM_FILE, expect.anything())
expect(setCurrentTab).toHaveBeenNthCalledWith(3, CreateFromDSLModalTab.FROM_URL, expect.anything())
})
})
// Tests for props variations
describe('Props Variations', () => {
it('should handle FROM_FILE as currentTab prop', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
expect(fileTab).toHaveClass('text-text-primary')
})
it('should handle FROM_URL as currentTab prop', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_URL}
setCurrentTab={setCurrentTab}
/>,
)
const urlTab = screen.getByText('app.importFromDSLUrl')
expect(urlTab).toHaveClass('text-text-primary')
})
it('should work with different setCurrentTab callback functions', () => {
const setCurrentTab1 = jest.fn()
const { rerender } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab1}
/>,
)
fireEvent.click(screen.getByText('app.importFromDSLUrl'))
expect(setCurrentTab1).toHaveBeenCalledWith(CreateFromDSLModalTab.FROM_URL, expect.anything())
const setCurrentTab2 = jest.fn()
rerender(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab2}
/>,
)
fireEvent.click(screen.getByText('app.importFromDSLUrl'))
expect(setCurrentTab2).toHaveBeenCalledWith(CreateFromDSLModalTab.FROM_URL, expect.anything())
})
})
// Tests for edge cases
describe('Edge Cases', () => {
it('should handle component mounting without errors', () => {
const setCurrentTab = jest.fn()
expect(() =>
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
),
).not.toThrow()
})
it('should handle component unmounting without errors', () => {
const setCurrentTab = jest.fn()
const { unmount } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
expect(() => unmount()).not.toThrow()
})
it('should handle currentTab prop change', () => {
const setCurrentTab = jest.fn()
const { rerender } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
// Initially FROM_FILE is active
let fileTab = screen.getByText('app.importFromDSLFile')
expect(fileTab).toHaveClass('text-text-primary')
// Change to FROM_URL
rerender(
<Tab
currentTab={CreateFromDSLModalTab.FROM_URL}
setCurrentTab={setCurrentTab}
/>,
)
// Now FROM_URL should be active
const urlTab = screen.getByText('app.importFromDSLUrl')
fileTab = screen.getByText('app.importFromDSLFile')
expect(urlTab).toHaveClass('text-text-primary')
expect(fileTab).not.toHaveClass('text-text-primary')
})
it('should handle multiple rerenders', () => {
const setCurrentTab = jest.fn()
const { rerender } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
rerender(
<Tab
currentTab={CreateFromDSLModalTab.FROM_URL}
setCurrentTab={setCurrentTab}
/>,
)
rerender(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
expect(fileTab).toHaveClass('text-text-primary')
})
it('should maintain DOM structure after multiple interactions', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const initialTabCount = container.querySelectorAll('.cursor-pointer').length
// Multiple clicks
fireEvent.click(screen.getByText('app.importFromDSLUrl'))
fireEvent.click(screen.getByText('app.importFromDSLFile'))
const afterClicksTabCount = container.querySelectorAll('.cursor-pointer').length
expect(afterClicksTabCount).toBe(initialTabCount)
})
})
// Tests for Item component integration
describe('Item Component Integration', () => {
it('should render Item components with correct cursor style', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const tabItems = container.querySelectorAll('.cursor-pointer')
expect(tabItems.length).toBe(2)
})
it('should pass correct isActive prop to Item components', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
const urlTab = screen.getByText('app.importFromDSLUrl')
// File tab should be active
expect(fileTab).toHaveClass('text-text-primary')
// URL tab should be inactive
expect(urlTab).not.toHaveClass('text-text-primary')
})
it('should pass correct label to Item components', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
expect(screen.getByText('app.importFromDSLFile')).toBeInTheDocument()
expect(screen.getByText('app.importFromDSLUrl')).toBeInTheDocument()
})
it('should pass correct onClick handler to Item components', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
const urlTab = screen.getByText('app.importFromDSLUrl')
fireEvent.click(fileTab)
fireEvent.click(urlTab)
expect(setCurrentTab).toHaveBeenCalledTimes(2)
expect(setCurrentTab).toHaveBeenNthCalledWith(1, CreateFromDSLModalTab.FROM_FILE, expect.anything())
expect(setCurrentTab).toHaveBeenNthCalledWith(2, CreateFromDSLModalTab.FROM_URL, expect.anything())
})
})
// Tests for accessibility
describe('Accessibility', () => {
it('should have clickable elements for each tab', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const clickableElements = container.querySelectorAll('.cursor-pointer')
expect(clickableElements.length).toBe(2)
})
it('should have visible text labels for each tab', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileLabel = screen.getByText('app.importFromDSLFile')
const urlLabel = screen.getByText('app.importFromDSLUrl')
expect(fileLabel).toBeVisible()
expect(urlLabel).toBeVisible()
})
it('should visually distinguish active tab from inactive tabs', () => {
const setCurrentTab = jest.fn()
const { container } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
// Active tab has indicator bar
const indicatorBars = container.querySelectorAll('.bg-util-colors-blue-brand-blue-brand-600')
expect(indicatorBars.length).toBe(1)
// Active tab has different text color
const fileTab = screen.getByText('app.importFromDSLFile')
expect(fileTab).toHaveClass('text-text-primary')
})
})
// Tests for component stability
describe('Component Stability', () => {
it('should handle rapid mount/unmount cycles', () => {
const setCurrentTab = jest.fn()
for (let i = 0; i < 5; i++) {
const { unmount } = render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
unmount()
}
expect(true).toBe(true)
})
it('should handle rapid tab switching', () => {
const setCurrentTab = jest.fn()
render(
<Tab
currentTab={CreateFromDSLModalTab.FROM_FILE}
setCurrentTab={setCurrentTab}
/>,
)
const fileTab = screen.getByText('app.importFromDSLFile')
const urlTab = screen.getByText('app.importFromDSLUrl')
// Rapid clicks
for (let i = 0; i < 10; i++)
fireEvent.click(i % 2 === 0 ? urlTab : fileTab)
expect(setCurrentTab).toHaveBeenCalledTimes(10)
})
})
})

View File

@@ -0,0 +1,439 @@
import React from 'react'
import { fireEvent, render, screen } from '@testing-library/react'
import CreateFromPipeline from './index'
// Mock list component to avoid deep dependency issues
jest.mock('./list', () => ({
__esModule: true,
default: () => <div data-testid="list">List Component</div>,
}))
// Mock CreateFromDSLModal to avoid deep dependency chain
jest.mock('./create-options/create-from-dsl-modal', () => ({
__esModule: true,
default: ({ show, onClose, onSuccess }: { show: boolean; onClose: () => void; onSuccess: () => void }) => (
show
? (
<div data-testid="dsl-modal">
<button data-testid="dsl-modal-close" onClick={onClose}>Close</button>
<button data-testid="dsl-modal-success" onClick={onSuccess}>Import Success</button>
</div>
)
: null
),
CreateFromDSLModalTab: {
FROM_URL: 'from-url',
},
}))
// Mock next/navigation
const mockReplace = jest.fn()
const mockPush = jest.fn()
let mockSearchParams = new URLSearchParams()
jest.mock('next/navigation', () => ({
useRouter: () => ({
replace: mockReplace,
push: mockPush,
}),
useSearchParams: () => mockSearchParams,
}))
// Mock react-i18next
jest.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
// Mock useInvalidDatasetList hook
const mockInvalidDatasetList = jest.fn()
jest.mock('@/service/knowledge/use-dataset', () => ({
useInvalidDatasetList: () => mockInvalidDatasetList,
}))
describe('CreateFromPipeline', () => {
beforeEach(() => {
jest.clearAllMocks()
mockSearchParams = new URLSearchParams()
})
// Tests for basic rendering
describe('Rendering', () => {
it('should render without crashing', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toBeInTheDocument()
})
it('should render the main container with correct className', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('relative')
expect(mainContainer).toHaveClass('flex')
expect(mainContainer).toHaveClass('h-[calc(100vh-56px)]')
expect(mainContainer).toHaveClass('flex-col')
expect(mainContainer).toHaveClass('overflow-hidden')
expect(mainContainer).toHaveClass('rounded-t-2xl')
expect(mainContainer).toHaveClass('border-t')
expect(mainContainer).toHaveClass('border-effects-highlight')
expect(mainContainer).toHaveClass('bg-background-default-subtle')
})
it('should render Header component with back to knowledge text', () => {
render(<CreateFromPipeline />)
expect(screen.getByText('datasetPipeline.creation.backToKnowledge')).toBeInTheDocument()
})
it('should render List component', () => {
render(<CreateFromPipeline />)
expect(screen.getByTestId('list')).toBeInTheDocument()
})
it('should render Footer component with import DSL button', () => {
render(<CreateFromPipeline />)
expect(screen.getByText('datasetPipeline.creation.importDSL')).toBeInTheDocument()
})
it('should render Effect component with blur effect', () => {
const { container } = render(<CreateFromPipeline />)
const effectElement = container.querySelector('.blur-\\[80px\\]')
expect(effectElement).toBeInTheDocument()
})
it('should render Effect component with correct positioning classes', () => {
const { container } = render(<CreateFromPipeline />)
const effectElement = container.querySelector('.left-8.top-\\[-34px\\].opacity-20')
expect(effectElement).toBeInTheDocument()
})
})
// Tests for Header component integration
describe('Header Component Integration', () => {
it('should render header with navigation link', () => {
render(<CreateFromPipeline />)
const link = screen.getByRole('link')
expect(link).toHaveAttribute('href', '/datasets')
})
it('should render back button inside header', () => {
render(<CreateFromPipeline />)
const button = screen.getByRole('button', { name: '' })
expect(button).toBeInTheDocument()
expect(button).toHaveClass('rounded-full')
})
it('should render header with correct styling', () => {
const { container } = render(<CreateFromPipeline />)
const headerElement = container.querySelector('.px-16.pb-2.pt-5')
expect(headerElement).toBeInTheDocument()
})
})
// Tests for Footer component integration
describe('Footer Component Integration', () => {
it('should render footer with import DSL button', () => {
render(<CreateFromPipeline />)
const importButton = screen.getByText('datasetPipeline.creation.importDSL')
expect(importButton).toBeInTheDocument()
})
it('should render footer at bottom with correct positioning classes', () => {
const { container } = render(<CreateFromPipeline />)
const footer = container.querySelector('.absolute.bottom-0.left-0.right-0')
expect(footer).toBeInTheDocument()
})
it('should render footer with backdrop blur', () => {
const { container } = render(<CreateFromPipeline />)
const footer = container.querySelector('.backdrop-blur-\\[6px\\]')
expect(footer).toBeInTheDocument()
})
it('should render divider in footer', () => {
const { container } = render(<CreateFromPipeline />)
// Divider renders with w-8 class
const divider = container.querySelector('.w-8')
expect(divider).toBeInTheDocument()
})
it('should open import modal when import DSL button is clicked', () => {
render(<CreateFromPipeline />)
const importButton = screen.getByText('datasetPipeline.creation.importDSL')
fireEvent.click(importButton)
expect(screen.getByTestId('dsl-modal')).toBeInTheDocument()
})
it('should not show import modal initially', () => {
render(<CreateFromPipeline />)
expect(screen.queryByTestId('dsl-modal')).not.toBeInTheDocument()
})
})
// Tests for Effect component integration
describe('Effect Component Integration', () => {
it('should render Effect with blur effect', () => {
const { container } = render(<CreateFromPipeline />)
const effectElement = container.querySelector('.blur-\\[80px\\]')
expect(effectElement).toBeInTheDocument()
})
it('should render Effect with absolute positioning', () => {
const { container } = render(<CreateFromPipeline />)
const effectElement = container.querySelector('.absolute.size-\\[112px\\].rounded-full')
expect(effectElement).toBeInTheDocument()
})
it('should render Effect with brand color', () => {
const { container } = render(<CreateFromPipeline />)
const effectElement = container.querySelector('.bg-util-colors-blue-brand-blue-brand-500')
expect(effectElement).toBeInTheDocument()
})
it('should render Effect with custom opacity', () => {
const { container } = render(<CreateFromPipeline />)
const effectElement = container.querySelector('.opacity-20')
expect(effectElement).toBeInTheDocument()
})
})
// Tests for layout structure
describe('Layout Structure', () => {
it('should render children in correct order', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
const children = mainContainer.children
// Should have 4 children: Effect, Header, List, Footer
expect(children.length).toBe(4)
})
it('should have flex column layout', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('flex-col')
})
it('should have overflow hidden on main container', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('overflow-hidden')
})
it('should have correct height calculation', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('h-[calc(100vh-56px)]')
})
})
// Tests for styling
describe('Styling', () => {
it('should have border styling on main container', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('border-t')
expect(mainContainer).toHaveClass('border-effects-highlight')
})
it('should have rounded top corners', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('rounded-t-2xl')
})
it('should have subtle background color', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('bg-background-default-subtle')
})
it('should have relative positioning for child absolute positioning', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('relative')
})
})
// Tests for edge cases
describe('Edge Cases', () => {
it('should handle component mounting without errors', () => {
expect(() => render(<CreateFromPipeline />)).not.toThrow()
})
it('should handle component unmounting without errors', () => {
const { unmount } = render(<CreateFromPipeline />)
expect(() => unmount()).not.toThrow()
})
it('should handle multiple renders without issues', () => {
const { rerender } = render(<CreateFromPipeline />)
rerender(<CreateFromPipeline />)
rerender(<CreateFromPipeline />)
rerender(<CreateFromPipeline />)
expect(screen.getByText('datasetPipeline.creation.backToKnowledge')).toBeInTheDocument()
})
it('should maintain consistent DOM structure across rerenders', () => {
const { container, rerender } = render(<CreateFromPipeline />)
const initialChildCount = (container.firstChild as HTMLElement)?.children.length
rerender(<CreateFromPipeline />)
const afterRerenderChildCount = (container.firstChild as HTMLElement)?.children.length
expect(afterRerenderChildCount).toBe(initialChildCount)
})
it('should handle remoteInstallUrl search param', () => {
mockSearchParams = new URLSearchParams('remoteInstallUrl=https://example.com/dsl.yaml')
render(<CreateFromPipeline />)
// Should render without crashing when remoteInstallUrl is present
expect(screen.getByText('datasetPipeline.creation.backToKnowledge')).toBeInTheDocument()
})
})
// Tests for accessibility
describe('Accessibility', () => {
it('should have accessible link for navigation', () => {
render(<CreateFromPipeline />)
const link = screen.getByRole('link')
expect(link).toBeInTheDocument()
expect(link).toHaveAttribute('href', '/datasets')
})
it('should have accessible buttons', () => {
render(<CreateFromPipeline />)
const buttons = screen.getAllByRole('button')
expect(buttons.length).toBeGreaterThanOrEqual(2) // back button and import DSL button
})
it('should use semantic structure for content', () => {
const { container } = render(<CreateFromPipeline />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer.tagName).toBe('DIV')
})
})
// Tests for component stability
describe('Component Stability', () => {
it('should not cause memory leaks on unmount', () => {
const { unmount } = render(<CreateFromPipeline />)
unmount()
expect(true).toBe(true)
})
it('should handle rapid mount/unmount cycles', () => {
for (let i = 0; i < 5; i++) {
const { unmount } = render(<CreateFromPipeline />)
unmount()
}
expect(true).toBe(true)
})
})
// Tests for user interactions
describe('User Interactions', () => {
it('should toggle import modal when clicking import DSL button', () => {
render(<CreateFromPipeline />)
// Initially modal is not shown
expect(screen.queryByTestId('dsl-modal')).not.toBeInTheDocument()
// Click import DSL button
const importButton = screen.getByText('datasetPipeline.creation.importDSL')
fireEvent.click(importButton)
// Modal should be shown
expect(screen.getByTestId('dsl-modal')).toBeInTheDocument()
})
it('should close modal when close button is clicked', () => {
render(<CreateFromPipeline />)
// Open modal
const importButton = screen.getByText('datasetPipeline.creation.importDSL')
fireEvent.click(importButton)
expect(screen.getByTestId('dsl-modal')).toBeInTheDocument()
// Click close button
const closeButton = screen.getByTestId('dsl-modal-close')
fireEvent.click(closeButton)
// Modal should be hidden
expect(screen.queryByTestId('dsl-modal')).not.toBeInTheDocument()
})
it('should close modal and redirect when close button is clicked with remoteInstallUrl', () => {
mockSearchParams = new URLSearchParams('remoteInstallUrl=https://example.com/dsl.yaml')
render(<CreateFromPipeline />)
// Open modal
const importButton = screen.getByText('datasetPipeline.creation.importDSL')
fireEvent.click(importButton)
// Click close button
const closeButton = screen.getByTestId('dsl-modal-close')
fireEvent.click(closeButton)
// Should call replace to remove the URL param
expect(mockReplace).toHaveBeenCalledWith('/datasets/create-from-pipeline')
})
it('should call invalidDatasetList when import is successful', () => {
render(<CreateFromPipeline />)
// Open modal
const importButton = screen.getByText('datasetPipeline.creation.importDSL')
fireEvent.click(importButton)
// Click success button
const successButton = screen.getByTestId('dsl-modal-success')
fireEvent.click(successButton)
// Should call invalidDatasetList
expect(mockInvalidDatasetList).toHaveBeenCalled()
})
})
})

View File

@@ -0,0 +1,842 @@
import { render, screen } from '@testing-library/react'
import List from './index'
import type { PipelineTemplate, PipelineTemplateListResponse } from '@/models/pipeline'
import { ChunkingMode } from '@/models/datasets'
// Mock i18n context
let mockLocale = 'en-US'
jest.mock('@/context/i18n', () => ({
useI18N: () => ({
locale: mockLocale,
}),
}))
// Mock global public store
let mockEnableMarketplace = true
jest.mock('@/context/global-public-context', () => ({
useGlobalPublicStore: (selector: (state: { systemFeatures: { enable_marketplace: boolean } }) => boolean) =>
selector({ systemFeatures: { enable_marketplace: mockEnableMarketplace } }),
}))
// Mock pipeline service hooks
let mockBuiltInPipelineData: PipelineTemplateListResponse | undefined
let mockBuiltInIsLoading = false
let mockCustomizedPipelineData: PipelineTemplateListResponse | undefined
let mockCustomizedIsLoading = false
jest.mock('@/service/use-pipeline', () => ({
usePipelineTemplateList: (params: { type: 'built-in' | 'customized'; language?: string }, enabled?: boolean) => {
if (params.type === 'built-in') {
return {
data: enabled !== false ? mockBuiltInPipelineData : undefined,
isLoading: mockBuiltInIsLoading,
}
}
return {
data: mockCustomizedPipelineData,
isLoading: mockCustomizedIsLoading,
}
},
}))
// Mock CreateCard component to avoid deep service dependencies
jest.mock('./create-card', () => ({
__esModule: true,
default: () => (
<div data-testid="create-card" className="h-[132px] cursor-pointer">
<span>datasetPipeline.creation.createFromScratch.title</span>
<span>datasetPipeline.creation.createFromScratch.description</span>
</div>
),
}))
// Mock TemplateCard component to avoid deep service dependencies
jest.mock('./template-card', () => ({
__esModule: true,
default: ({ pipeline, type, showMoreOperations }: {
pipeline: PipelineTemplate
type: 'built-in' | 'customized'
showMoreOperations?: boolean
}) => (
<div
data-testid={`template-card-${pipeline.id}`}
data-type={type}
data-show-more={showMoreOperations}
className="h-[132px]"
>
<span data-testid={`template-name-${pipeline.id}`}>{pipeline.name}</span>
<span data-testid={`template-description-${pipeline.id}`}>{pipeline.description}</span>
<span data-testid={`template-chunk-structure-${pipeline.id}`}>{pipeline.chunk_structure}</span>
</div>
),
}))
// Factory function for creating mock pipeline templates
const createMockPipelineTemplate = (overrides: Partial<PipelineTemplate> = {}): PipelineTemplate => ({
id: 'template-1',
name: 'Test Pipeline',
description: 'Test pipeline description',
icon: {
icon_type: 'emoji',
icon: '🔧',
icon_background: '#FFEAD5',
icon_url: '',
},
position: 1,
chunk_structure: ChunkingMode.text,
...overrides,
})
describe('List', () => {
beforeEach(() => {
jest.clearAllMocks()
mockLocale = 'en-US'
mockEnableMarketplace = true
mockBuiltInPipelineData = undefined
mockBuiltInIsLoading = false
mockCustomizedPipelineData = undefined
mockCustomizedIsLoading = false
})
/**
* List Component Container
* Tests for the main List wrapper component rendering and styling
*/
describe('List Component Container', () => {
it('should render without crashing', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toBeInTheDocument()
})
it('should render the main container as a div element', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer.tagName).toBe('DIV')
})
it('should render the main container with grow class for flex expansion', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('grow')
})
it('should render the main container with gap-y-1 class for vertical spacing', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('gap-y-1')
})
it('should render the main container with overflow-y-auto for vertical scrolling', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('overflow-y-auto')
})
it('should render the main container with horizontal padding px-16', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('px-16')
})
it('should render the main container with bottom padding pb-[60px]', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('pb-[60px]')
})
it('should render the main container with top padding pt-1', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('pt-1')
})
it('should have all required styling classes applied', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('grow')
expect(mainContainer).toHaveClass('gap-y-1')
expect(mainContainer).toHaveClass('overflow-y-auto')
expect(mainContainer).toHaveClass('px-16')
expect(mainContainer).toHaveClass('pb-[60px]')
expect(mainContainer).toHaveClass('pt-1')
})
it('should render both BuiltInPipelineList and CustomizedList as children when customized data exists', () => {
mockCustomizedPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'custom-child-test' })],
}
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
// BuiltInPipelineList always renders (1 child)
// CustomizedList renders when it has data (adds more children: title + grid)
// So we should have at least 2 children when customized data exists
expect(mainContainer.children.length).toBeGreaterThanOrEqual(2)
})
it('should render only BuiltInPipelineList when customized list is empty', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
// CustomizedList returns null when empty, so only BuiltInPipelineList renders
expect(mainContainer.children.length).toBe(1)
})
})
/**
* BuiltInPipelineList Integration
* Tests for built-in pipeline templates list including CreateCard and TemplateCards
*/
describe('BuiltInPipelineList Integration', () => {
it('should render CreateCard component', () => {
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.creation.createFromScratch.title')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.creation.createFromScratch.description')).toBeInTheDocument()
})
it('should render grid container with correct responsive classes', () => {
const { container } = render(<List />)
const gridContainer = container.querySelector('.grid')
expect(gridContainer).toBeInTheDocument()
expect(gridContainer).toHaveClass('grid-cols-1')
expect(gridContainer).toHaveClass('gap-3')
expect(gridContainer).toHaveClass('py-2')
expect(gridContainer).toHaveClass('sm:grid-cols-2')
expect(gridContainer).toHaveClass('md:grid-cols-3')
expect(gridContainer).toHaveClass('lg:grid-cols-4')
})
it('should not render built-in template cards when loading', () => {
mockBuiltInIsLoading = true
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate()],
}
render(<List />)
expect(screen.queryByTestId('template-card-template-1')).not.toBeInTheDocument()
})
it('should render built-in template cards when data is loaded', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'built-1', name: 'Pipeline 1' }),
createMockPipelineTemplate({ id: 'built-2', name: 'Pipeline 2' }),
],
}
render(<List />)
expect(screen.getByTestId('template-card-built-1')).toBeInTheDocument()
expect(screen.getByTestId('template-card-built-2')).toBeInTheDocument()
expect(screen.getByText('Pipeline 1')).toBeInTheDocument()
expect(screen.getByText('Pipeline 2')).toBeInTheDocument()
})
it('should render empty state when no built-in templates (only CreateCard visible)', () => {
mockBuiltInPipelineData = {
pipeline_templates: [],
}
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
expect(screen.queryByTestId(/^template-card-/)).not.toBeInTheDocument()
})
it('should handle undefined pipeline_templates gracefully', () => {
mockBuiltInPipelineData = {} as PipelineTemplateListResponse
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
it('should pass type=built-in to TemplateCard', () => {
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'built-type-test' })],
}
render(<List />)
const templateCard = screen.getByTestId('template-card-built-type-test')
expect(templateCard).toHaveAttribute('data-type', 'built-in')
})
it('should pass showMoreOperations=false to built-in TemplateCards', () => {
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'built-ops-test' })],
}
render(<List />)
const templateCard = screen.getByTestId('template-card-built-ops-test')
expect(templateCard).toHaveAttribute('data-show-more', 'false')
})
it('should render multiple built-in templates in order', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'first', name: 'First' }),
createMockPipelineTemplate({ id: 'second', name: 'Second' }),
createMockPipelineTemplate({ id: 'third', name: 'Third' }),
],
}
const { container } = render(<List />)
const gridContainer = container.querySelector('.grid')
const cards = gridContainer?.querySelectorAll('[data-testid^="template-card-"]')
expect(cards?.length).toBe(3)
})
})
/**
* CustomizedList Integration
* Tests for customized pipeline templates list including conditional rendering
*/
describe('CustomizedList Integration', () => {
it('should return null when loading', () => {
mockCustomizedIsLoading = true
render(<List />)
expect(screen.queryByText('datasetPipeline.templates.customized')).not.toBeInTheDocument()
})
it('should return null when list is empty', () => {
mockCustomizedPipelineData = {
pipeline_templates: [],
}
render(<List />)
expect(screen.queryByText('datasetPipeline.templates.customized')).not.toBeInTheDocument()
})
it('should return null when pipeline_templates is undefined', () => {
mockCustomizedPipelineData = {} as PipelineTemplateListResponse
render(<List />)
expect(screen.queryByText('datasetPipeline.templates.customized')).not.toBeInTheDocument()
})
it('should render customized section title when data is available', () => {
mockCustomizedPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'custom-1' })],
}
render(<List />)
expect(screen.getByText('datasetPipeline.templates.customized')).toBeInTheDocument()
})
it('should render customized title with correct styling', () => {
mockCustomizedPipelineData = {
pipeline_templates: [createMockPipelineTemplate()],
}
const { container } = render(<List />)
const title = container.querySelector('.system-sm-semibold-uppercase')
expect(title).toBeInTheDocument()
expect(title).toHaveClass('pt-2')
expect(title).toHaveClass('text-text-tertiary')
})
it('should render customized template cards', () => {
mockCustomizedPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'custom-1', name: 'Custom Pipeline 1' }),
],
}
render(<List />)
expect(screen.getByTestId('template-card-custom-1')).toBeInTheDocument()
expect(screen.getByText('Custom Pipeline 1')).toBeInTheDocument()
})
it('should render multiple customized templates', () => {
mockCustomizedPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'custom-1', name: 'Custom 1' }),
createMockPipelineTemplate({ id: 'custom-2', name: 'Custom 2' }),
createMockPipelineTemplate({ id: 'custom-3', name: 'Custom 3' }),
],
}
render(<List />)
expect(screen.getByText('Custom 1')).toBeInTheDocument()
expect(screen.getByText('Custom 2')).toBeInTheDocument()
expect(screen.getByText('Custom 3')).toBeInTheDocument()
})
it('should pass type=customized to TemplateCard', () => {
mockCustomizedPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'custom-type-test' })],
}
render(<List />)
const templateCard = screen.getByTestId('template-card-custom-type-test')
expect(templateCard).toHaveAttribute('data-type', 'customized')
})
it('should not pass showMoreOperations prop to customized TemplateCards (defaults to true)', () => {
mockCustomizedPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'custom-ops-test' })],
}
render(<List />)
const templateCard = screen.getByTestId('template-card-custom-ops-test')
// showMoreOperations is not passed, so data-show-more should be undefined
expect(templateCard).not.toHaveAttribute('data-show-more', 'false')
})
it('should render customized grid with responsive classes', () => {
mockCustomizedPipelineData = {
pipeline_templates: [createMockPipelineTemplate()],
}
const { container } = render(<List />)
// Find the second grid (customized list grid)
const grids = container.querySelectorAll('.grid')
expect(grids.length).toBe(2) // built-in grid and customized grid
expect(grids[1]).toHaveClass('grid-cols-1')
expect(grids[1]).toHaveClass('gap-3')
expect(grids[1]).toHaveClass('py-2')
})
})
/**
* Language Handling
* Tests for locale-based language selection in BuiltInPipelineList
*/
describe('Language Handling', () => {
it('should use zh-Hans locale when set', () => {
mockLocale = 'zh-Hans'
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate()],
}
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
it('should use ja-JP locale when set', () => {
mockLocale = 'ja-JP'
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate()],
}
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
it('should fallback to default language for unsupported locales', () => {
mockLocale = 'fr-FR'
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate()],
}
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
it('should handle ko-KR locale (fallback)', () => {
mockLocale = 'ko-KR'
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
})
/**
* Marketplace Feature Flag
* Tests for enable_marketplace system feature affecting built-in templates fetching
*/
describe('Marketplace Feature Flag', () => {
it('should not fetch built-in templates when marketplace is disabled', () => {
mockEnableMarketplace = false
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ name: 'Should Not Show' })],
}
render(<List />)
// CreateCard should render but template should not (enabled=false)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
expect(screen.queryByText('Should Not Show')).not.toBeInTheDocument()
})
it('should fetch built-in templates when marketplace is enabled', () => {
mockEnableMarketplace = true
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'marketplace', name: 'Marketplace Template' })],
}
render(<List />)
expect(screen.getByText('Marketplace Template')).toBeInTheDocument()
})
})
/**
* Template Data Rendering
* Tests for correct rendering of template properties (name, description, chunk_structure)
*/
describe('Template Data Rendering', () => {
it('should render template name correctly', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'name-test', name: 'My Custom Pipeline Name' }),
],
}
render(<List />)
expect(screen.getByTestId('template-name-name-test')).toHaveTextContent('My Custom Pipeline Name')
})
it('should render template description correctly', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'desc-test', description: 'This is a detailed description' }),
],
}
render(<List />)
expect(screen.getByTestId('template-description-desc-test')).toHaveTextContent('This is a detailed description')
})
it('should render template with text chunk structure', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'chunk-text', chunk_structure: ChunkingMode.text }),
],
}
render(<List />)
expect(screen.getByTestId('template-chunk-structure-chunk-text')).toHaveTextContent(ChunkingMode.text)
})
it('should render template with qa chunk structure', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'chunk-qa', chunk_structure: ChunkingMode.qa }),
],
}
render(<List />)
expect(screen.getByTestId('template-chunk-structure-chunk-qa')).toHaveTextContent(ChunkingMode.qa)
})
it('should render template with parentChild chunk structure', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'chunk-pc', chunk_structure: ChunkingMode.parentChild }),
],
}
render(<List />)
expect(screen.getByTestId('template-chunk-structure-chunk-pc')).toHaveTextContent(ChunkingMode.parentChild)
})
})
/**
* Edge Cases
* Tests for boundary conditions, special characters, and component lifecycle
*/
describe('Edge Cases', () => {
it('should handle component mounting without errors', () => {
expect(() => render(<List />)).not.toThrow()
})
it('should handle component unmounting without errors', () => {
const { unmount } = render(<List />)
expect(() => unmount()).not.toThrow()
})
it('should handle multiple rerenders without issues', () => {
const { rerender } = render(<List />)
rerender(<List />)
rerender(<List />)
rerender(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
})
it('should maintain consistent DOM structure across rerenders', () => {
const { container, rerender } = render(<List />)
const initialChildCount = (container.firstChild as HTMLElement)?.children.length
rerender(<List />)
const afterRerenderChildCount = (container.firstChild as HTMLElement)?.children.length
expect(afterRerenderChildCount).toBe(initialChildCount)
})
it('should handle concurrent built-in and customized templates', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'built-in-1', name: 'Built-in Template' }),
],
}
mockCustomizedPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'custom-1', name: 'Customized Template' }),
],
}
render(<List />)
expect(screen.getByText('Built-in Template')).toBeInTheDocument()
expect(screen.getByText('Customized Template')).toBeInTheDocument()
expect(screen.getByText('datasetPipeline.templates.customized')).toBeInTheDocument()
})
it('should handle templates with long names gracefully', () => {
const longName = 'A'.repeat(100)
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'long-name', name: longName }),
],
}
render(<List />)
expect(screen.getByTestId('template-name-long-name')).toHaveTextContent(longName)
})
it('should handle templates with empty description', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'empty-desc', description: '' }),
],
}
render(<List />)
expect(screen.getByTestId('template-description-empty-desc')).toHaveTextContent('')
})
it('should handle templates with special characters in name', () => {
mockBuiltInPipelineData = {
pipeline_templates: [
createMockPipelineTemplate({ id: 'special', name: 'Test <>&"\'Pipeline' }),
],
}
render(<List />)
expect(screen.getByTestId('template-name-special')).toHaveTextContent('Test <>&"\'Pipeline')
})
it('should handle rapid mount/unmount cycles', () => {
for (let i = 0; i < 5; i++) {
const { unmount } = render(<List />)
unmount()
}
expect(true).toBe(true)
})
})
/**
* Loading States
* Tests for component behavior during data loading
*/
describe('Loading States', () => {
it('should handle both lists loading simultaneously', () => {
mockBuiltInIsLoading = true
mockCustomizedIsLoading = true
render(<List />)
expect(screen.getByTestId('create-card')).toBeInTheDocument()
expect(screen.queryByText('datasetPipeline.templates.customized')).not.toBeInTheDocument()
})
it('should handle built-in loading while customized is loaded', () => {
mockBuiltInIsLoading = true
mockCustomizedPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'custom-only', name: 'Customized Only' })],
}
render(<List />)
expect(screen.getByText('Customized Only')).toBeInTheDocument()
})
it('should handle customized loading while built-in is loaded', () => {
mockCustomizedIsLoading = true
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'built-only', name: 'Built-in Only' })],
}
render(<List />)
expect(screen.getByText('Built-in Only')).toBeInTheDocument()
expect(screen.queryByText('datasetPipeline.templates.customized')).not.toBeInTheDocument()
})
it('should transition from loading to loaded state', () => {
mockBuiltInIsLoading = true
const { rerender } = render(<List />)
expect(screen.queryByTestId('template-card-transition')).not.toBeInTheDocument()
// Simulate data loaded
mockBuiltInIsLoading = false
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'transition', name: 'After Load' })],
}
rerender(<List />)
expect(screen.getByText('After Load')).toBeInTheDocument()
})
})
/**
* Component Stability
* Tests for consistent rendering and state management
*/
describe('Component Stability', () => {
it('should render same structure on initial render and rerender', () => {
const { container, rerender } = render(<List />)
const initialHTML = container.innerHTML
rerender(<List />)
const rerenderHTML = container.innerHTML
expect(rerenderHTML).toBe(initialHTML)
})
it('should not cause memory leaks on unmount', () => {
const { unmount } = render(<List />)
unmount()
expect(true).toBe(true)
})
it('should handle state changes correctly', () => {
mockBuiltInPipelineData = undefined
const { rerender } = render(<List />)
// Add data
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate({ id: 'state-test', name: 'State Test' })],
}
rerender(<List />)
expect(screen.getByText('State Test')).toBeInTheDocument()
})
})
/**
* Accessibility
* Tests for semantic structure and keyboard navigation support
*/
describe('Accessibility', () => {
it('should use semantic div structure for main container', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer.tagName).toBe('DIV')
})
it('should have scrollable container for keyboard navigation', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('overflow-y-auto')
})
it('should have appropriate spacing for readability', () => {
const { container } = render(<List />)
const mainContainer = container.firstChild as HTMLElement
expect(mainContainer).toHaveClass('gap-y-1')
expect(mainContainer).toHaveClass('px-16')
})
it('should render grid structure for template cards', () => {
mockBuiltInPipelineData = {
pipeline_templates: [createMockPipelineTemplate()],
}
const { container } = render(<List />)
const grid = container.querySelector('.grid')
expect(grid).toBeInTheDocument()
})
})
/**
* Large Datasets
* Tests for performance with many templates
*/
describe('Large Datasets', () => {
it('should handle many built-in templates', () => {
mockBuiltInPipelineData = {
pipeline_templates: Array.from({ length: 50 }, (_, i) =>
createMockPipelineTemplate({ id: `built-${i}`, name: `Pipeline ${i}` }),
),
}
render(<List />)
expect(screen.getByText('Pipeline 0')).toBeInTheDocument()
expect(screen.getByText('Pipeline 49')).toBeInTheDocument()
})
it('should handle many customized templates', () => {
mockCustomizedPipelineData = {
pipeline_templates: Array.from({ length: 50 }, (_, i) =>
createMockPipelineTemplate({ id: `custom-${i}`, name: `Custom ${i}` }),
),
}
render(<List />)
expect(screen.getByText('Custom 0')).toBeInTheDocument()
expect(screen.getByText('Custom 49')).toBeInTheDocument()
})
})
})

View File

@@ -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()
})
})
})

View File

@@ -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()
})
})
})
})