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