mirror of
https://github.com/langgenius/dify.git
synced 2026-01-06 06:26:00 +00:00
test: add unit tests for CreateFromPipeline and Tab components with comprehensive coverage
This commit is contained in:
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user