mirror of
https://github.com/langgenius/dify.git
synced 2026-03-28 17:40:53 +08:00
test: add comprehensive unit tests for RagPipeline components including conversion, publish modal, and toast notifications
This commit is contained in:
@ -3,44 +3,35 @@ import { cleanup, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
// Import real utility functions (pure functions, no side effects)
|
||||
|
||||
// Import mocked modules for manipulation
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { usePipelineInit } from './hooks'
|
||||
import RagPipelineWrapper from './index'
|
||||
import { processNodesWithoutDataSource } from './utils'
|
||||
import { usePipelineInit } from '../hooks'
|
||||
import RagPipelineWrapper from '../index'
|
||||
import { processNodesWithoutDataSource } from '../utils'
|
||||
|
||||
// Mock: Context - need to control return values
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock: Hook with API calls
|
||||
vi.mock('./hooks', () => ({
|
||||
vi.mock('../hooks', () => ({
|
||||
usePipelineInit: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock: Store creator
|
||||
vi.mock('./store', () => ({
|
||||
vi.mock('../store', () => ({
|
||||
createRagPipelineSliceSlice: vi.fn(() => ({})),
|
||||
}))
|
||||
|
||||
// Mock: Utility with complex workflow dependencies (generateNewNode, etc.)
|
||||
vi.mock('./utils', () => ({
|
||||
vi.mock('../utils', () => ({
|
||||
processNodesWithoutDataSource: vi.fn((nodes, viewport) => ({
|
||||
nodes,
|
||||
viewport,
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock: Complex component with useParams, Toast, API calls
|
||||
vi.mock('./components/conversion', () => ({
|
||||
vi.mock('../components/conversion', () => ({
|
||||
default: () => <div data-testid="conversion-component">Conversion Component</div>,
|
||||
}))
|
||||
|
||||
// Mock: Complex component with many hooks and workflow dependencies
|
||||
vi.mock('./components/rag-pipeline-main', () => ({
|
||||
vi.mock('../components/rag-pipeline-main', () => ({
|
||||
default: ({ nodes, edges, viewport }: { nodes?: unknown[], edges?: unknown[], viewport?: { zoom?: number } }) => (
|
||||
<div data-testid="rag-pipeline-main">
|
||||
<span data-testid="nodes-count">{nodes?.length ?? 0}</span>
|
||||
@ -50,27 +41,22 @@ vi.mock('./components/rag-pipeline-main', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock: Complex component with ReactFlow and many providers
|
||||
vi.mock('@/app/components/workflow', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="workflow-default-context">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock: Context provider
|
||||
vi.mock('@/app/components/workflow/context', () => ({
|
||||
WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="workflow-context-provider">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Type assertions for mocked functions
|
||||
const mockUseDatasetDetailContextWithSelector = vi.mocked(useDatasetDetailContextWithSelector)
|
||||
const mockUsePipelineInit = vi.mocked(usePipelineInit)
|
||||
const mockProcessNodesWithoutDataSource = vi.mocked(processNodesWithoutDataSource)
|
||||
|
||||
// Helper to mock selector with actual execution (increases function coverage)
|
||||
// This executes the real selector function: s => s.dataset?.pipeline_id
|
||||
const mockSelectorWithDataset = (pipelineId: string | null | undefined) => {
|
||||
mockUseDatasetDetailContextWithSelector.mockImplementation((selector: (state: Record<string, unknown>) => unknown) => {
|
||||
const mockState = { dataset: pipelineId ? { pipeline_id: pipelineId } : null }
|
||||
@ -78,7 +64,6 @@ const mockSelectorWithDataset = (pipelineId: string | null | undefined) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Test data factory
|
||||
const createMockWorkflowData = (overrides?: Partial<FetchWorkflowDraftResponse>): FetchWorkflowDraftResponse => ({
|
||||
graph: {
|
||||
nodes: [
|
||||
@ -157,7 +142,6 @@ describe('RagPipelineWrapper', () => {
|
||||
|
||||
describe('RagPipeline', () => {
|
||||
beforeEach(() => {
|
||||
// Default setup for RagPipeline tests - execute real selector function
|
||||
mockSelectorWithDataset('pipeline-123')
|
||||
})
|
||||
|
||||
@ -167,7 +151,6 @@ describe('RagPipeline', () => {
|
||||
|
||||
render(<RagPipelineWrapper />)
|
||||
|
||||
// Real Loading component has role="status"
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -240,8 +223,6 @@ describe('RagPipeline', () => {
|
||||
|
||||
render(<RagPipelineWrapper />)
|
||||
|
||||
// initialNodes is a real function - verify nodes are rendered
|
||||
// The real initialNodes processes nodes and adds position data
|
||||
expect(screen.getByTestId('rag-pipeline-main')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -251,7 +232,6 @@ describe('RagPipeline', () => {
|
||||
|
||||
render(<RagPipelineWrapper />)
|
||||
|
||||
// initialEdges is a real function - verify component renders with edges
|
||||
expect(screen.getByTestId('edges-count').textContent).toBe('1')
|
||||
})
|
||||
|
||||
@ -269,7 +249,6 @@ describe('RagPipeline', () => {
|
||||
|
||||
render(<RagPipelineWrapper />)
|
||||
|
||||
// When data is undefined, Loading is shown, processNodesWithoutDataSource is not called
|
||||
expect(mockProcessNodesWithoutDataSource).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -279,13 +258,10 @@ describe('RagPipeline', () => {
|
||||
|
||||
const { rerender } = render(<RagPipelineWrapper />)
|
||||
|
||||
// Clear mock call count after initial render
|
||||
mockProcessNodesWithoutDataSource.mockClear()
|
||||
|
||||
// Rerender with same data reference (no change to mockUsePipelineInit)
|
||||
rerender(<RagPipelineWrapper />)
|
||||
|
||||
// processNodesWithoutDataSource should not be called again due to useMemo
|
||||
// Note: React strict mode may cause double render, so we check it's not excessive
|
||||
expect(mockProcessNodesWithoutDataSource.mock.calls.length).toBeLessThanOrEqual(1)
|
||||
})
|
||||
@ -467,14 +443,11 @@ describe('Conditional Rendering Flow', () => {
|
||||
it('should transition from loading to loaded state', () => {
|
||||
mockSelectorWithDataset('pipeline-123')
|
||||
|
||||
// Start with loading state
|
||||
mockUsePipelineInit.mockReturnValue({ data: undefined, isLoading: true })
|
||||
const { rerender } = render(<RagPipelineWrapper />)
|
||||
|
||||
// Real Loading component has role="status"
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
|
||||
// Transition to loaded state
|
||||
const mockData = createMockWorkflowData()
|
||||
mockUsePipelineInit.mockReturnValue({ data: mockData, isLoading: false })
|
||||
rerender(<RagPipelineWrapper />)
|
||||
@ -483,7 +456,6 @@ describe('Conditional Rendering Flow', () => {
|
||||
})
|
||||
|
||||
it('should switch from Conversion to Pipeline when pipelineId becomes available', () => {
|
||||
// Start without pipelineId
|
||||
mockSelectorWithDataset(null)
|
||||
mockUsePipelineInit.mockReturnValue({ data: undefined, isLoading: false })
|
||||
|
||||
@ -491,13 +463,11 @@ describe('Conditional Rendering Flow', () => {
|
||||
|
||||
expect(screen.getByTestId('conversion-component')).toBeInTheDocument()
|
||||
|
||||
// PipelineId becomes available
|
||||
mockSelectorWithDataset('new-pipeline-id')
|
||||
mockUsePipelineInit.mockReturnValue({ data: undefined, isLoading: true })
|
||||
rerender(<RagPipelineWrapper />)
|
||||
|
||||
expect(screen.queryByTestId('conversion-component')).not.toBeInTheDocument()
|
||||
// Real Loading component has role="status"
|
||||
expect(screen.getByRole('status')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -520,11 +490,8 @@ describe('Error Handling', () => {
|
||||
|
||||
mockUsePipelineInit.mockReturnValue({ data: mockData, isLoading: false })
|
||||
|
||||
// Suppress console.error for expected error
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
// Real initialNodes will throw when nodes is null
|
||||
// This documents the component's current behavior - it requires valid nodes array
|
||||
expect(() => render(<RagPipelineWrapper />)).toThrow()
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
@ -538,11 +505,8 @@ describe('Error Handling', () => {
|
||||
|
||||
mockUsePipelineInit.mockReturnValue({ data: mockData, isLoading: false })
|
||||
|
||||
// Suppress console.error for expected error
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
// When graph is undefined, component throws because data.graph.nodes is accessed
|
||||
// This documents the component's current behavior - it requires graph to be present
|
||||
expect(() => render(<RagPipelineWrapper />)).toThrow()
|
||||
|
||||
consoleSpy.mockRestore()
|
||||
@ -1,17 +1,10 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Conversion from './conversion'
|
||||
import Conversion from '../conversion'
|
||||
|
||||
const mockConvert = vi.fn()
|
||||
const mockInvalidDatasetDetail = vi.fn()
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'ds-123' }),
|
||||
}))
|
||||
@ -66,7 +59,7 @@ vi.mock('@/app/components/base/confirm', () => ({
|
||||
: null,
|
||||
}))
|
||||
|
||||
vi.mock('./screenshot', () => ({
|
||||
vi.mock('../screenshot', () => ({
|
||||
default: () => <div data-testid="screenshot" />,
|
||||
}))
|
||||
|
||||
@ -82,21 +75,21 @@ describe('Conversion', () => {
|
||||
it('should render conversion title and description', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.getByText('conversion.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('conversion.descriptionChunk1')).toBeInTheDocument()
|
||||
expect(screen.getByText('conversion.descriptionChunk2')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.conversion.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.conversion.descriptionChunk1')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.conversion.descriptionChunk2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render convert button', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.getByText('operations.convert')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.operations.convert')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render warning text', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
expect(screen.getByText('conversion.warning')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.conversion.warning')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render screenshot component', () => {
|
||||
@ -110,16 +103,16 @@ describe('Conversion', () => {
|
||||
|
||||
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
|
||||
|
||||
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
|
||||
expect(screen.getByText('conversion.confirm.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.conversion.confirm.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide confirm modal when cancel is clicked', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
|
||||
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByTestId('cancel-btn'))
|
||||
@ -129,7 +122,7 @@ describe('Conversion', () => {
|
||||
it('should call convert when confirm is clicked', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(mockConvert).toHaveBeenCalledWith('ds-123', expect.objectContaining({
|
||||
@ -146,7 +139,7 @@ describe('Conversion', () => {
|
||||
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(Toast.default.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
@ -163,7 +156,7 @@ describe('Conversion', () => {
|
||||
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(Toast.default.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
@ -179,7 +172,7 @@ describe('Conversion', () => {
|
||||
|
||||
render(<Conversion />)
|
||||
|
||||
fireEvent.click(screen.getByText('operations.convert'))
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.convert'))
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
|
||||
expect(Toast.default.notify).toHaveBeenCalledWith(expect.objectContaining({
|
||||
@ -3,29 +3,19 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||
|
||||
// ============================================================================
|
||||
// Import Components After Mocks Setup
|
||||
// ============================================================================
|
||||
import Conversion from '../conversion'
|
||||
import RagPipelinePanel from '../panel'
|
||||
import PublishAsKnowledgePipelineModal from '../publish-as-knowledge-pipeline-modal'
|
||||
import PublishToast from '../publish-toast'
|
||||
import RagPipelineChildren from '../rag-pipeline-children'
|
||||
import PipelineScreenShot from '../screenshot'
|
||||
|
||||
import Conversion from './conversion'
|
||||
import RagPipelinePanel from './panel'
|
||||
import PublishAsKnowledgePipelineModal from './publish-as-knowledge-pipeline-modal'
|
||||
import PublishToast from './publish-toast'
|
||||
import RagPipelineChildren from './rag-pipeline-children'
|
||||
import PipelineScreenShot from './screenshot'
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Dependencies - All vi.mock calls must come before any imports
|
||||
// ============================================================================
|
||||
|
||||
// Mock next/navigation
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'test-dataset-id' }),
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
// Mock next/image
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({ src, alt, width, height }: { src: string, alt: string, width: number, height: number }) => (
|
||||
// eslint-disable-next-line next/no-img-element
|
||||
@ -33,7 +23,6 @@ vi.mock('next/image', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock next/dynamic
|
||||
vi.mock('next/dynamic', () => ({
|
||||
default: (importFn: () => Promise<{ default: React.ComponentType<unknown> }>, options?: { ssr?: boolean }) => {
|
||||
const DynamicComponent = ({ children, ...props }: PropsWithChildren) => {
|
||||
@ -44,7 +33,6 @@ vi.mock('next/dynamic', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock workflow store - using controllable state
|
||||
let mockShowImportDSLModal = false
|
||||
const mockSetShowImportDSLModal = vi.fn((value: boolean) => {
|
||||
mockShowImportDSLModal = value
|
||||
@ -112,7 +100,6 @@ vi.mock('@/app/components/workflow/store', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Mock workflow hooks - extract mock functions for assertions using vi.hoisted
|
||||
const {
|
||||
mockHandlePaneContextmenuCancel,
|
||||
mockExportCheck,
|
||||
@ -148,8 +135,7 @@ vi.mock('@/app/components/workflow/hooks', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Mock rag-pipeline hooks
|
||||
vi.mock('../hooks', () => ({
|
||||
vi.mock('../../hooks', () => ({
|
||||
useAvailableNodesMetaData: () => ({}),
|
||||
useDSL: () => ({
|
||||
exportCheck: mockExportCheck,
|
||||
@ -178,18 +164,15 @@ vi.mock('../hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock rag-pipeline search hook
|
||||
vi.mock('../hooks/use-rag-pipeline-search', () => ({
|
||||
vi.mock('../../hooks/use-rag-pipeline-search', () => ({
|
||||
useRagPipelineSearch: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock configs-map hook
|
||||
vi.mock('../hooks/use-configs-map', () => ({
|
||||
vi.mock('../../hooks/use-configs-map', () => ({
|
||||
useConfigsMap: () => ({}),
|
||||
}))
|
||||
|
||||
// Mock inspect-vars-crud hook
|
||||
vi.mock('../hooks/use-inspect-vars-crud', () => ({
|
||||
vi.mock('../../hooks/use-inspect-vars-crud', () => ({
|
||||
useInspectVarsCrud: () => ({
|
||||
hasNodeInspectVars: vi.fn(),
|
||||
hasSetInspectVar: vi.fn(),
|
||||
@ -208,14 +191,12 @@ vi.mock('../hooks/use-inspect-vars-crud', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow hooks for fetch-workflow-inspect-vars
|
||||
vi.mock('@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars', () => ({
|
||||
useSetWorkflowVarsWithValue: () => ({
|
||||
fetchInspectVars: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock service hooks - with controllable convert function
|
||||
let mockConvertFn = vi.fn()
|
||||
let mockIsPending = false
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
@ -253,7 +234,6 @@ vi.mock('@/service/workflow', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock event emitter context - with controllable subscription
|
||||
let mockEventSubscriptionCallback: ((v: { type: string, payload?: { data?: EnvironmentVariable[] } }) => void) | null = null
|
||||
const mockUseSubscription = vi.fn((callback: (v: { type: string, payload?: { data?: EnvironmentVariable[] } }) => void) => {
|
||||
mockEventSubscriptionCallback = callback
|
||||
@ -267,7 +247,6 @@ vi.mock('@/context/event-emitter', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock toast
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: vi.fn(),
|
||||
@ -280,33 +259,28 @@ vi.mock('@/app/components/base/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock useTheme hook
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({
|
||||
theme: 'light',
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock basePath
|
||||
vi.mock('@/utils/var', () => ({
|
||||
basePath: '/public',
|
||||
}))
|
||||
|
||||
// Mock provider context
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => createMockProviderContextValue(),
|
||||
useProviderContextSelector: <T,>(selector: (state: ReturnType<typeof createMockProviderContextValue>) => T): T =>
|
||||
selector(createMockProviderContextValue()),
|
||||
}))
|
||||
|
||||
// Mock WorkflowWithInnerContext
|
||||
vi.mock('@/app/components/workflow', () => ({
|
||||
WorkflowWithInnerContext: ({ children }: PropsWithChildren) => (
|
||||
<div data-testid="workflow-inner-context">{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock workflow panel
|
||||
vi.mock('@/app/components/workflow/panel', () => ({
|
||||
default: ({ components }: { components?: { left?: React.ReactNode, right?: React.ReactNode } }) => (
|
||||
<div data-testid="workflow-panel">
|
||||
@ -316,19 +290,16 @@ vi.mock('@/app/components/workflow/panel', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock PluginDependency
|
||||
vi.mock('../../workflow/plugin-dependency', () => ({
|
||||
vi.mock('../../../workflow/plugin-dependency', () => ({
|
||||
default: () => <div data-testid="plugin-dependency" />,
|
||||
}))
|
||||
|
||||
// Mock plugin-dependency hooks
|
||||
vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
|
||||
usePluginDependencies: () => ({
|
||||
handleCheckPluginDependencies: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock DSLExportConfirmModal
|
||||
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
default: ({ envList, onConfirm, onClose }: { envList: EnvironmentVariable[], onConfirm: () => void, onClose: () => void }) => (
|
||||
<div data-testid="dsl-export-confirm-modal">
|
||||
@ -339,13 +310,11 @@ vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock workflow constants
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
||||
WORKFLOW_DATA_UPDATE: 'WORKFLOW_DATA_UPDATE',
|
||||
}))
|
||||
|
||||
// Mock workflow utils
|
||||
vi.mock('@/app/components/workflow/utils', () => ({
|
||||
initialNodes: vi.fn(nodes => nodes),
|
||||
initialEdges: vi.fn(edges => edges),
|
||||
@ -353,7 +322,6 @@ vi.mock('@/app/components/workflow/utils', () => ({
|
||||
getKeyboardKeyNameBySystem: (key: string) => key,
|
||||
}))
|
||||
|
||||
// Mock Confirm component
|
||||
vi.mock('@/app/components/base/confirm', () => ({
|
||||
default: ({ title, content, isShow, onConfirm, onCancel, isLoading, isDisabled }: {
|
||||
title: string
|
||||
@ -381,7 +349,6 @@ vi.mock('@/app/components/base/confirm', () => ({
|
||||
: null,
|
||||
}))
|
||||
|
||||
// Mock Modal component
|
||||
vi.mock('@/app/components/base/modal', () => ({
|
||||
default: ({ children, isShow, onClose, className }: PropsWithChildren<{
|
||||
isShow: boolean
|
||||
@ -396,7 +363,6 @@ vi.mock('@/app/components/base/modal', () => ({
|
||||
: null,
|
||||
}))
|
||||
|
||||
// Mock Input component
|
||||
vi.mock('@/app/components/base/input', () => ({
|
||||
default: ({ value, onChange, placeholder }: {
|
||||
value: string
|
||||
@ -412,7 +378,6 @@ vi.mock('@/app/components/base/input', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Textarea component
|
||||
vi.mock('@/app/components/base/textarea', () => ({
|
||||
default: ({ value, onChange, placeholder, className }: {
|
||||
value: string
|
||||
@ -430,7 +395,6 @@ vi.mock('@/app/components/base/textarea', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock AppIcon component
|
||||
vi.mock('@/app/components/base/app-icon', () => ({
|
||||
default: ({ onClick, iconType, icon, background, imageUrl, className, size }: {
|
||||
onClick?: () => void
|
||||
@ -454,7 +418,6 @@ vi.mock('@/app/components/base/app-icon', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock AppIconPicker component
|
||||
vi.mock('@/app/components/base/app-icon-picker', () => ({
|
||||
default: ({ onSelect, onClose }: {
|
||||
onSelect: (item: { type: string, icon?: string, background?: string, url?: string }) => void
|
||||
@ -478,7 +441,6 @@ vi.mock('@/app/components/base/app-icon-picker', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Uploader component
|
||||
vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({
|
||||
default: ({ file, updateFile, className, accept, displayName }: {
|
||||
file?: File
|
||||
@ -504,25 +466,21 @@ vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock use-context-selector
|
||||
vi.mock('use-context-selector', () => ({
|
||||
useContext: vi.fn(() => ({
|
||||
notify: vi.fn(),
|
||||
})),
|
||||
}))
|
||||
|
||||
// Mock RagPipelineHeader
|
||||
vi.mock('./rag-pipeline-header', () => ({
|
||||
vi.mock('../rag-pipeline-header', () => ({
|
||||
default: () => <div data-testid="rag-pipeline-header" />,
|
||||
}))
|
||||
|
||||
// Mock PublishToast
|
||||
vi.mock('./publish-toast', () => ({
|
||||
vi.mock('../publish-toast', () => ({
|
||||
default: () => <div data-testid="publish-toast" />,
|
||||
}))
|
||||
|
||||
// Mock UpdateDSLModal for RagPipelineChildren tests
|
||||
vi.mock('./update-dsl-modal', () => ({
|
||||
vi.mock('../update-dsl-modal', () => ({
|
||||
default: ({ onCancel, onBackup, onImport }: {
|
||||
onCancel: () => void
|
||||
onBackup: () => void
|
||||
@ -536,7 +494,6 @@ vi.mock('./update-dsl-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock DSLExportConfirmModal for RagPipelineChildren tests
|
||||
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
default: ({ envList, onConfirm, onClose }: {
|
||||
envList: EnvironmentVariable[]
|
||||
@ -555,18 +512,11 @@ vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Suites
|
||||
// ============================================================================
|
||||
|
||||
describe('Conversion', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render conversion component without crashing', () => {
|
||||
render(<Conversion />)
|
||||
@ -600,9 +550,6 @@ describe('Conversion', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// User Interactions Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should show confirm modal when convert button is clicked', () => {
|
||||
render(<Conversion />)
|
||||
@ -617,20 +564,15 @@ describe('Conversion', () => {
|
||||
it('should hide confirm modal when cancel is clicked', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
// Open modal
|
||||
const convertButton = screen.getByRole('button', { name: /datasetPipeline\.operations\.convert/i })
|
||||
fireEvent.click(convertButton)
|
||||
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
|
||||
|
||||
// Cancel modal
|
||||
fireEvent.click(screen.getByTestId('cancel-btn'))
|
||||
expect(screen.queryByTestId('confirm-modal')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// API Callback Tests - covers lines 21-39
|
||||
// --------------------------------------------------------------------------
|
||||
describe('API Callbacks', () => {
|
||||
beforeEach(() => {
|
||||
mockConvertFn = vi.fn()
|
||||
@ -638,14 +580,12 @@ describe('Conversion', () => {
|
||||
})
|
||||
|
||||
it('should call convert with datasetId and show success toast on success', async () => {
|
||||
// Setup mock to capture and call onSuccess callback
|
||||
mockConvertFn.mockImplementation((_datasetId: string, options: { onSuccess: (res: { status: string }) => void }) => {
|
||||
options.onSuccess({ status: 'success' })
|
||||
})
|
||||
|
||||
render(<Conversion />)
|
||||
|
||||
// Open modal and confirm
|
||||
const convertButton = screen.getByRole('button', { name: /datasetPipeline\.operations\.convert/i })
|
||||
fireEvent.click(convertButton)
|
||||
fireEvent.click(screen.getByTestId('confirm-btn'))
|
||||
@ -690,7 +630,6 @@ describe('Conversion', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockConvertFn).toHaveBeenCalled()
|
||||
})
|
||||
// Modal should still be visible since conversion failed
|
||||
expect(screen.getByTestId('confirm-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -711,32 +650,23 @@ describe('Conversion', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Conversion is exported with React.memo
|
||||
expect((Conversion as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
|
||||
it('should use useCallback for handleConvert', () => {
|
||||
const { rerender } = render(<Conversion />)
|
||||
|
||||
// Rerender should not cause issues with callback
|
||||
rerender(<Conversion />)
|
||||
expect(screen.getByRole('button', { name: /datasetPipeline\.operations\.convert/i })).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle missing datasetId gracefully', () => {
|
||||
render(<Conversion />)
|
||||
|
||||
// Component should render without crashing
|
||||
expect(screen.getByText('datasetPipeline.conversion.title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -747,9 +677,6 @@ describe('PipelineScreenShot', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<PipelineScreenShot />)
|
||||
@ -770,14 +697,10 @@ describe('PipelineScreenShot', () => {
|
||||
render(<PipelineScreenShot />)
|
||||
|
||||
const img = screen.getByTestId('mock-image')
|
||||
// Default theme is 'light' from mock
|
||||
expect(img).toHaveAttribute('src', '/public/screenshots/light/Pipeline.png')
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
expect((PipelineScreenShot as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
@ -790,9 +713,6 @@ describe('PublishToast', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Note: PublishToast is mocked, so we just verify the mock renders
|
||||
@ -802,12 +722,8 @@ describe('PublishToast', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be defined', () => {
|
||||
// The real PublishToast is mocked, but we can verify the import
|
||||
expect(PublishToast).toBeDefined()
|
||||
})
|
||||
})
|
||||
@ -826,9 +742,6 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
onConfirm: mockOnConfirm,
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render modal with title', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
@ -863,9 +776,6 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// User Interactions Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should update name when input changes', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
@ -906,11 +816,9 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
// Update values
|
||||
fireEvent.change(screen.getByTestId('input'), { target: { value: ' Trimmed Name ' } })
|
||||
fireEvent.change(screen.getByTestId('textarea'), { target: { value: ' Trimmed Description ' } })
|
||||
|
||||
// Click publish
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.publish/i }))
|
||||
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith(
|
||||
@ -931,52 +839,39 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should update icon when emoji is selected', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
// Open picker
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
|
||||
// Select emoji
|
||||
fireEvent.click(screen.getByTestId('select-emoji'))
|
||||
|
||||
// Picker should close
|
||||
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update icon when image is selected', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
// Open picker
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
|
||||
// Select image
|
||||
fireEvent.click(screen.getByTestId('select-image'))
|
||||
|
||||
// Picker should close
|
||||
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close picker and restore icon when picker is closed', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
// Open picker
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
expect(screen.getByTestId('app-icon-picker')).toBeInTheDocument()
|
||||
|
||||
// Close picker
|
||||
fireEvent.click(screen.getByTestId('close-picker'))
|
||||
|
||||
// Picker should close
|
||||
expect(screen.queryByTestId('app-icon-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Props Validation Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Props Validation', () => {
|
||||
it('should disable publish button when name is empty', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
// Clear the name
|
||||
fireEvent.change(screen.getByTestId('input'), { target: { value: '' } })
|
||||
|
||||
const publishButton = screen.getByRole('button', { name: /workflow\.common\.publish/i })
|
||||
@ -986,7 +881,6 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should disable publish button when name is only whitespace', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
// Set whitespace-only name
|
||||
fireEvent.change(screen.getByTestId('input'), { target: { value: ' ' } })
|
||||
|
||||
const publishButton = screen.getByRole('button', { name: /workflow\.common\.publish/i })
|
||||
@ -1009,14 +903,10 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should use useCallback for handleSelectIcon', () => {
|
||||
const { rerender } = render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
// Rerender should not cause issues
|
||||
rerender(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
expect(screen.getByTestId('app-icon')).toBeInTheDocument()
|
||||
})
|
||||
@ -1028,9 +918,6 @@ describe('RagPipelinePanel', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render panel component without crashing', () => {
|
||||
render(<RagPipelinePanel />)
|
||||
@ -1046,9 +933,6 @@ describe('RagPipelinePanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with memo', () => {
|
||||
expect((RagPipelinePanel as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
@ -1063,9 +947,6 @@ describe('RagPipelineChildren', () => {
|
||||
mockEventSubscriptionCallback = null
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<RagPipelineChildren />)
|
||||
@ -1090,9 +971,6 @@ describe('RagPipelineChildren', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Event Subscription Tests - covers lines 37-40
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Event Subscription', () => {
|
||||
it('should subscribe to event emitter', () => {
|
||||
render(<RagPipelineChildren />)
|
||||
@ -1103,12 +981,10 @@ describe('RagPipelineChildren', () => {
|
||||
it('should handle DSL_EXPORT_CHECK event and set secretEnvList', async () => {
|
||||
render(<RagPipelineChildren />)
|
||||
|
||||
// Simulate DSL_EXPORT_CHECK event
|
||||
const mockEnvVariables: EnvironmentVariable[] = [
|
||||
{ id: '1', name: 'SECRET_KEY', value: 'test-secret', value_type: 'secret' as const, description: '' },
|
||||
]
|
||||
|
||||
// Trigger the subscription callback
|
||||
if (mockEventSubscriptionCallback) {
|
||||
mockEventSubscriptionCallback({
|
||||
type: 'DSL_EXPORT_CHECK',
|
||||
@ -1116,7 +992,6 @@ describe('RagPipelineChildren', () => {
|
||||
})
|
||||
}
|
||||
|
||||
// DSLExportConfirmModal should be rendered
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('dsl-export-confirm-modal')).toBeInTheDocument()
|
||||
})
|
||||
@ -1125,7 +1000,6 @@ describe('RagPipelineChildren', () => {
|
||||
it('should not show DSLExportConfirmModal for non-DSL_EXPORT_CHECK events', () => {
|
||||
render(<RagPipelineChildren />)
|
||||
|
||||
// Trigger a different event type
|
||||
if (mockEventSubscriptionCallback) {
|
||||
mockEventSubscriptionCallback({
|
||||
type: 'OTHER_EVENT',
|
||||
@ -1136,9 +1010,6 @@ describe('RagPipelineChildren', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// UpdateDSLModal Handlers Tests - covers lines 48-51
|
||||
// --------------------------------------------------------------------------
|
||||
describe('UpdateDSLModal Handlers', () => {
|
||||
beforeEach(() => {
|
||||
mockShowImportDSLModal = true
|
||||
@ -1168,14 +1039,10 @@ describe('RagPipelineChildren', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// DSLExportConfirmModal Tests - covers lines 55-60
|
||||
// --------------------------------------------------------------------------
|
||||
describe('DSLExportConfirmModal', () => {
|
||||
it('should render DSLExportConfirmModal when secretEnvList has items', async () => {
|
||||
render(<RagPipelineChildren />)
|
||||
|
||||
// Simulate DSL_EXPORT_CHECK event with secrets
|
||||
const mockEnvVariables: EnvironmentVariable[] = [
|
||||
{ id: '1', name: 'API_KEY', value: 'secret-value', value_type: 'secret' as const, description: '' },
|
||||
]
|
||||
@ -1195,7 +1062,6 @@ describe('RagPipelineChildren', () => {
|
||||
it('should close DSLExportConfirmModal when onClose is triggered', async () => {
|
||||
render(<RagPipelineChildren />)
|
||||
|
||||
// First show the modal
|
||||
const mockEnvVariables: EnvironmentVariable[] = [
|
||||
{ id: '1', name: 'API_KEY', value: 'secret-value', value_type: 'secret' as const, description: '' },
|
||||
]
|
||||
@ -1211,7 +1077,6 @@ describe('RagPipelineChildren', () => {
|
||||
expect(screen.getByTestId('dsl-export-confirm-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Close the modal
|
||||
fireEvent.click(screen.getByTestId('dsl-export-close'))
|
||||
|
||||
await waitFor(() => {
|
||||
@ -1222,7 +1087,6 @@ describe('RagPipelineChildren', () => {
|
||||
it('should call handleExportDSL when onConfirm is triggered', async () => {
|
||||
render(<RagPipelineChildren />)
|
||||
|
||||
// Show the modal
|
||||
const mockEnvVariables: EnvironmentVariable[] = [
|
||||
{ id: '1', name: 'API_KEY', value: 'secret-value', value_type: 'secret' as const, description: '' },
|
||||
]
|
||||
@ -1238,16 +1102,12 @@ describe('RagPipelineChildren', () => {
|
||||
expect(screen.getByTestId('dsl-export-confirm-modal')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Confirm export
|
||||
fireEvent.click(screen.getByTestId('dsl-export-confirm'))
|
||||
|
||||
expect(mockHandleExportDSL).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with memo', () => {
|
||||
expect((RagPipelineChildren as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
@ -1255,10 +1115,6 @@ describe('RagPipelineChildren', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1276,17 +1132,13 @@ describe('Integration Tests', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Update name
|
||||
fireEvent.change(screen.getByTestId('input'), { target: { value: 'My Pipeline' } })
|
||||
|
||||
// Add description
|
||||
fireEvent.change(screen.getByTestId('textarea'), { target: { value: 'A great pipeline' } })
|
||||
|
||||
// Change icon
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
fireEvent.click(screen.getByTestId('select-emoji'))
|
||||
|
||||
// Publish
|
||||
fireEvent.click(screen.getByRole('button', { name: /workflow\.common\.publish/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
@ -1304,10 +1156,6 @@ describe('Integration Tests', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases
|
||||
// ============================================================================
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1322,7 +1170,6 @@ describe('Edge Cases', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Clear the name
|
||||
const input = screen.getByTestId('input')
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
expect(input).toHaveValue('')
|
||||
@ -1360,10 +1207,6 @@ describe('Edge Cases', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Accessibility Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Accessibility', () => {
|
||||
describe('Conversion', () => {
|
||||
it('should have accessible button', () => {
|
||||
@ -1,13 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import PublishAsKnowledgePipelineModal from './publish-as-knowledge-pipeline-modal'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import PublishAsKnowledgePipelineModal from '../publish-as-knowledge-pipeline-modal'
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
@ -105,7 +99,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
expect(screen.getByTestId('modal')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.publishAs')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.publishAs')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should initialize with knowledgeName from store', () => {
|
||||
@ -133,7 +127,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should call onCancel when cancel button clicked', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('operation.cancel'))
|
||||
fireEvent.click(screen.getByText('common.operation.cancel'))
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
@ -141,7 +135,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
it('should call onConfirm with name, icon, and description when confirm clicked', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.publish'))
|
||||
fireEvent.click(screen.getByText('workflow.common.publish'))
|
||||
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith(
|
||||
'Test Pipeline',
|
||||
@ -174,21 +168,21 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
const nameInput = screen.getByTestId('name-input')
|
||||
fireEvent.change(nameInput, { target: { value: '' } })
|
||||
|
||||
const confirmBtn = screen.getByText('common.publish')
|
||||
const confirmBtn = screen.getByText('workflow.common.publish')
|
||||
expect(confirmBtn).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should disable confirm button when confirmDisabled is true', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} confirmDisabled />)
|
||||
|
||||
const confirmBtn = screen.getByText('common.publish')
|
||||
const confirmBtn = screen.getByText('workflow.common.publish')
|
||||
expect(confirmBtn).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not call onConfirm when confirmDisabled is true', () => {
|
||||
render(<PublishAsKnowledgePipelineModal {...defaultProps} confirmDisabled />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.publish'))
|
||||
fireEvent.click(screen.getByText('workflow.common.publish'))
|
||||
|
||||
expect(mockOnConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
@ -209,7 +203,6 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
fireEvent.click(screen.getByTestId('app-icon'))
|
||||
fireEvent.click(screen.getByTestId('select-emoji'))
|
||||
|
||||
// Icon picker should close
|
||||
expect(screen.queryByTestId('icon-picker')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -240,7 +233,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
|
||||
const textarea = screen.getByTestId('description-textarea')
|
||||
fireEvent.change(textarea, { target: { value: ' Some desc ' } })
|
||||
|
||||
fireEvent.click(screen.getByText('common.publish'))
|
||||
fireEvent.click(screen.getByText('workflow.common.publish'))
|
||||
|
||||
expect(mockOnConfirm).toHaveBeenCalledWith(
|
||||
'Trimmed Name',
|
||||
@ -1,15 +1,7 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import PublishToast from './publish-toast'
|
||||
import PublishToast from '../publish-toast'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow store with controllable state
|
||||
let mockPublishedAt = 0
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: (selector: (state: Record<string, unknown>) => unknown) => {
|
||||
@ -32,19 +24,19 @@ describe('PublishToast', () => {
|
||||
mockPublishedAt = 0
|
||||
render(<PublishToast />)
|
||||
|
||||
expect(screen.getByText('publishToast.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.publishToast.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render toast title', () => {
|
||||
render(<PublishToast />)
|
||||
|
||||
expect(screen.getByText('publishToast.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.publishToast.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render toast description', () => {
|
||||
render(<PublishToast />)
|
||||
|
||||
expect(screen.getByText('publishToast.desc')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.publishToast.desc')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render when publishedAt is set', () => {
|
||||
@ -57,14 +49,13 @@ describe('PublishToast', () => {
|
||||
it('should have correct positioning classes', () => {
|
||||
render(<PublishToast />)
|
||||
|
||||
const container = screen.getByText('publishToast.title').closest('.absolute')
|
||||
const container = screen.getByText('pipeline.publishToast.title').closest('.absolute')
|
||||
expect(container).toHaveClass('bottom-[45px]', 'left-0', 'right-0', 'z-10')
|
||||
})
|
||||
|
||||
it('should render info icon', () => {
|
||||
const { container } = render(<PublishToast />)
|
||||
|
||||
// The RiInformation2Fill icon should be rendered
|
||||
const iconContainer = container.querySelector('.text-text-accent')
|
||||
expect(iconContainer).toBeInTheDocument()
|
||||
})
|
||||
@ -72,7 +63,6 @@ describe('PublishToast', () => {
|
||||
it('should render close button', () => {
|
||||
const { container } = render(<PublishToast />)
|
||||
|
||||
// The close button is a div with cursor-pointer, not a semantic button
|
||||
const closeButton = container.querySelector('.cursor-pointer')
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
})
|
||||
@ -82,25 +72,23 @@ describe('PublishToast', () => {
|
||||
it('should hide toast when close button is clicked', () => {
|
||||
const { container } = render(<PublishToast />)
|
||||
|
||||
// The close button is a div with cursor-pointer, not a semantic button
|
||||
const closeButton = container.querySelector('.cursor-pointer')
|
||||
expect(screen.getByText('publishToast.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.publishToast.title')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
expect(screen.queryByText('publishToast.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('pipeline.publishToast.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should remain hidden after close button is clicked', () => {
|
||||
const { container, rerender } = render(<PublishToast />)
|
||||
|
||||
// The close button is a div with cursor-pointer, not a semantic button
|
||||
const closeButton = container.querySelector('.cursor-pointer')
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
rerender(<PublishToast />)
|
||||
|
||||
expect(screen.queryByText('publishToast.title')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('pipeline.publishToast.title')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -115,14 +103,14 @@ describe('PublishToast', () => {
|
||||
it('should have correct toast width', () => {
|
||||
render(<PublishToast />)
|
||||
|
||||
const toastContainer = screen.getByText('publishToast.title').closest('.w-\\[420px\\]')
|
||||
const toastContainer = screen.getByText('pipeline.publishToast.title').closest('.w-\\[420px\\]')
|
||||
expect(toastContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have rounded border', () => {
|
||||
render(<PublishToast />)
|
||||
|
||||
const toastContainer = screen.getByText('publishToast.title').closest('.rounded-xl')
|
||||
const toastContainer = screen.getByText('pipeline.publishToast.title').closest('.rounded-xl')
|
||||
expect(toastContainer).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -2,10 +2,9 @@ import type { PropsWithChildren } from 'react'
|
||||
import type { Edge, Node, Viewport } from 'reactflow'
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import RagPipelineMain from './rag-pipeline-main'
|
||||
import RagPipelineMain from '../rag-pipeline-main'
|
||||
|
||||
// Mock hooks from ../hooks
|
||||
vi.mock('../hooks', () => ({
|
||||
vi.mock('../../hooks', () => ({
|
||||
useAvailableNodesMetaData: () => ({ nodes: [], nodesMap: {} }),
|
||||
useDSL: () => ({
|
||||
exportCheck: vi.fn(),
|
||||
@ -34,8 +33,7 @@ vi.mock('../hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useConfigsMap
|
||||
vi.mock('../hooks/use-configs-map', () => ({
|
||||
vi.mock('../../hooks/use-configs-map', () => ({
|
||||
useConfigsMap: () => ({
|
||||
flowId: 'test-flow-id',
|
||||
flowType: 'ragPipeline',
|
||||
@ -43,8 +41,7 @@ vi.mock('../hooks/use-configs-map', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useInspectVarsCrud
|
||||
vi.mock('../hooks/use-inspect-vars-crud', () => ({
|
||||
vi.mock('../../hooks/use-inspect-vars-crud', () => ({
|
||||
useInspectVarsCrud: () => ({
|
||||
hasNodeInspectVars: vi.fn(),
|
||||
hasSetInspectVar: vi.fn(),
|
||||
@ -63,7 +60,6 @@ vi.mock('../hooks/use-inspect-vars-crud', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
const mockSetRagPipelineVariables = vi.fn()
|
||||
const mockSetEnvironmentVariables = vi.fn()
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
@ -75,14 +71,12 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow hooks
|
||||
vi.mock('@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars', () => ({
|
||||
useSetWorkflowVarsWithValue: () => ({
|
||||
fetchInspectVars: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock WorkflowWithInnerContext
|
||||
vi.mock('@/app/components/workflow', () => ({
|
||||
WorkflowWithInnerContext: ({ children, onWorkflowDataUpdate }: PropsWithChildren<{ onWorkflowDataUpdate?: (payload: unknown) => void }>) => (
|
||||
<div data-testid="workflow-inner-context">
|
||||
@ -108,8 +102,7 @@ vi.mock('@/app/components/workflow', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock RagPipelineChildren
|
||||
vi.mock('./rag-pipeline-children', () => ({
|
||||
vi.mock('../rag-pipeline-children', () => ({
|
||||
default: () => <div data-testid="rag-pipeline-children">Children</div>,
|
||||
}))
|
||||
|
||||
@ -201,7 +194,6 @@ describe('RagPipelineMain', () => {
|
||||
it('should use useNodesSyncDraft hook', () => {
|
||||
render(<RagPipelineMain {...defaultProps} />)
|
||||
|
||||
// If the component renders, the hook was called successfully
|
||||
expect(screen.getByTestId('workflow-inner-context')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -2,7 +2,7 @@ import type { PropsWithChildren } from 'react'
|
||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DSLImportStatus } from '@/models/app'
|
||||
import UpdateDSLModal from './update-dsl-modal'
|
||||
import UpdateDSLModal from '../update-dsl-modal'
|
||||
|
||||
class MockFileReader {
|
||||
onload: ((this: FileReader, event: ProgressEvent<FileReader>) => void) | null = null
|
||||
@ -15,25 +15,15 @@ class MockFileReader {
|
||||
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader)
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock use-context-selector
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('use-context-selector', () => ({
|
||||
useContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
|
||||
// Mock toast context
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
ToastContext: { Provider: ({ children }: PropsWithChildren) => children },
|
||||
}))
|
||||
|
||||
// Mock event emitter
|
||||
const mockEmit = vi.fn()
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
@ -41,7 +31,6 @@ vi.mock('@/context/event-emitter', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
getState: () => ({
|
||||
@ -50,13 +39,11 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow utils
|
||||
vi.mock('@/app/components/workflow/utils', () => ({
|
||||
initialNodes: (nodes: unknown[]) => nodes,
|
||||
initialEdges: (edges: unknown[]) => edges,
|
||||
}))
|
||||
|
||||
// Mock plugin dependencies
|
||||
const mockHandleCheckPluginDependencies = vi.fn()
|
||||
vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
|
||||
usePluginDependencies: () => ({
|
||||
@ -64,7 +51,6 @@ vi.mock('@/app/components/workflow/plugin-dependency/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock pipeline service
|
||||
const mockImportDSL = vi.fn()
|
||||
const mockImportDSLConfirm = vi.fn()
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
@ -72,7 +58,6 @@ vi.mock('@/service/use-pipeline', () => ({
|
||||
useImportPipelineDSLConfirm: () => ({ mutateAsync: mockImportDSLConfirm }),
|
||||
}))
|
||||
|
||||
// Mock workflow service
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: vi.fn().mockResolvedValue({
|
||||
graph: { nodes: [], edges: [], viewport: { x: 0, y: 0, zoom: 1 } },
|
||||
@ -81,7 +66,6 @@ vi.mock('@/service/workflow', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Uploader
|
||||
vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({
|
||||
default: ({ updateFile }: { updateFile: (file?: File) => void }) => (
|
||||
<div data-testid="uploader">
|
||||
@ -103,7 +87,6 @@ vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Button
|
||||
vi.mock('@/app/components/base/button', () => ({
|
||||
default: ({ children, onClick, disabled, className, variant, loading }: {
|
||||
children: React.ReactNode
|
||||
@ -125,7 +108,6 @@ vi.mock('@/app/components/base/button', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Modal
|
||||
vi.mock('@/app/components/base/modal', () => ({
|
||||
default: ({ children, isShow, _onClose, className }: PropsWithChildren<{
|
||||
isShow: boolean
|
||||
@ -140,7 +122,6 @@ vi.mock('@/app/components/base/modal', () => ({
|
||||
: null,
|
||||
}))
|
||||
|
||||
// Mock workflow constants
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
WORKFLOW_DATA_UPDATE: 'WORKFLOW_DATA_UPDATE',
|
||||
}))
|
||||
@ -176,15 +157,13 @@ describe('UpdateDSLModal', () => {
|
||||
it('should render title', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The component uses t('common.importDSL', { ns: 'workflow' }) which returns 'common.importDSL'
|
||||
expect(screen.getByText('common.importDSL')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.importDSL')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render warning tip', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The component uses t('common.importDSLTip', { ns: 'workflow' })
|
||||
expect(screen.getByText('common.importDSLTip')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.importDSLTip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render uploader', () => {
|
||||
@ -196,29 +175,25 @@ describe('UpdateDSLModal', () => {
|
||||
it('should render backup button', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The component uses t('common.backupCurrentDraft', { ns: 'workflow' })
|
||||
expect(screen.getByText('common.backupCurrentDraft')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.backupCurrentDraft')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render cancel button', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The component uses t('newApp.Cancel', { ns: 'app' })
|
||||
expect(screen.getByText('newApp.Cancel')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.Cancel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render import button', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The component uses t('common.overwriteAndImport', { ns: 'workflow' })
|
||||
expect(screen.getByText('common.overwriteAndImport')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.overwriteAndImport')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render choose DSL section', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The component uses t('common.chooseDSL', { ns: 'workflow' })
|
||||
expect(screen.getByText('common.chooseDSL')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.chooseDSL')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -226,7 +201,7 @@ describe('UpdateDSLModal', () => {
|
||||
it('should call onCancel when cancel button is clicked', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
const cancelButton = screen.getByText('newApp.Cancel')
|
||||
const cancelButton = screen.getByText('app.newApp.Cancel')
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
@ -235,7 +210,7 @@ describe('UpdateDSLModal', () => {
|
||||
it('should call onBackup when backup button is clicked', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
const backupButton = screen.getByText('common.backupCurrentDraft')
|
||||
const backupButton = screen.getByText('workflow.common.backupCurrentDraft')
|
||||
fireEvent.click(backupButton)
|
||||
|
||||
expect(mockOnBackup).toHaveBeenCalled()
|
||||
@ -249,7 +224,6 @@ describe('UpdateDSLModal', () => {
|
||||
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
// File should be processed
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('uploader')).toBeInTheDocument()
|
||||
})
|
||||
@ -261,14 +235,12 @@ describe('UpdateDSLModal', () => {
|
||||
const clearButton = screen.getByTestId('clear-file')
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
// File should be cleared
|
||||
expect(screen.getByTestId('uploader')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onCancel when close icon is clicked', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The close icon is in a div with onClick={onCancel}
|
||||
const closeIconContainer = document.querySelector('.cursor-pointer')
|
||||
if (closeIconContainer) {
|
||||
fireEvent.click(closeIconContainer)
|
||||
@ -281,7 +253,7 @@ describe('UpdateDSLModal', () => {
|
||||
it('should show import button disabled when no file is selected', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).toBeDisabled()
|
||||
})
|
||||
|
||||
@ -294,7 +266,7 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -302,22 +274,20 @@ describe('UpdateDSLModal', () => {
|
||||
it('should disable import button after file is cleared', async () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// First select a file
|
||||
const fileInput = screen.getByTestId('file-input')
|
||||
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Clear the file
|
||||
const clearButton = screen.getByTestId('clear-file')
|
||||
fireEvent.click(clearButton)
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -344,15 +314,14 @@ describe('UpdateDSLModal', () => {
|
||||
it('should render import button with warning variant', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).toHaveAttribute('data-variant', 'warning')
|
||||
})
|
||||
|
||||
it('should render backup button with secondary variant', () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// The backup button text is inside a nested div, so we need to find the closest button
|
||||
const backupButtonText = screen.getByText('common.backupCurrentDraft')
|
||||
const backupButtonText = screen.getByText('workflow.common.backupCurrentDraft')
|
||||
const backupButton = backupButtonText.closest('button')
|
||||
expect(backupButton).toHaveAttribute('data-variant', 'secondary')
|
||||
})
|
||||
@ -362,22 +331,18 @@ describe('UpdateDSLModal', () => {
|
||||
it('should call importDSL when import button is clicked with file content', async () => {
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// Select a file
|
||||
const fileInput = screen.getByTestId('file-input')
|
||||
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
// Wait for FileReader to process
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Click import button
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
// Wait for import to be called
|
||||
await waitFor(() => {
|
||||
expect(mockImportDSL).toHaveBeenCalled()
|
||||
})
|
||||
@ -392,17 +357,16 @@ describe('UpdateDSLModal', () => {
|
||||
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// Select a file and click import
|
||||
const fileInput = screen.getByTestId('file-input')
|
||||
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -426,11 +390,11 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -452,11 +416,11 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -478,11 +442,11 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -506,11 +470,11 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -533,13 +497,12 @@ describe('UpdateDSLModal', () => {
|
||||
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
// Wait for FileReader to process and button to be enabled
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -558,13 +521,12 @@ describe('UpdateDSLModal', () => {
|
||||
const file = new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
// Wait for FileReader to complete and button to be enabled
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -588,16 +550,15 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Flush the FileReader microtask to ensure fileContent is set
|
||||
await act(async () => {
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve))
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -619,11 +580,11 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -649,23 +610,20 @@ describe('UpdateDSLModal', () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
// Flush microtasks scheduled by the FileReader mock (which uses queueMicrotask)
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve))
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(importButton)
|
||||
// Flush the promise resolution from mockImportDSL
|
||||
await Promise.resolve()
|
||||
// Advance past the 300ms setTimeout in the component
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
vi.useRealTimers()
|
||||
@ -687,14 +645,13 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
// Wait for error modal with version info
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1.0.0')).toBeInTheDocument()
|
||||
expect(screen.getByText('2.0.0')).toBeInTheDocument()
|
||||
@ -717,20 +674,18 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
// Wait for error modal
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Find and click cancel button in error modal - it should be the one with secondary variant
|
||||
const cancelButtons = screen.getAllByText('newApp.Cancel')
|
||||
const cancelButtons = screen.getAllByText('app.newApp.Cancel')
|
||||
const errorModalCancelButton = cancelButtons.find(btn =>
|
||||
btn.getAttribute('data-variant') === 'secondary',
|
||||
)
|
||||
@ -738,9 +693,8 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.click(errorModalCancelButton)
|
||||
}
|
||||
|
||||
// Modal should be closed
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('newApp.appCreateDSLErrorTitle')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('app.newApp.appCreateDSLErrorTitle')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -767,27 +721,23 @@ describe('UpdateDSLModal', () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
// Flush microtasks scheduled by the FileReader mock (which uses queueMicrotask)
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve))
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(importButton)
|
||||
// Flush the promise resolution from mockImportDSL
|
||||
await Promise.resolve()
|
||||
// Advance past the 300ms setTimeout in the component
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
// Click confirm button
|
||||
const confirmButton = screen.getByText('newApp.Confirm')
|
||||
const confirmButton = screen.getByText('app.newApp.Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -818,18 +768,18 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const confirmButton = screen.getByText('newApp.Confirm')
|
||||
const confirmButton = screen.getByText('app.newApp.Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -860,18 +810,18 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const confirmButton = screen.getByText('newApp.Confirm')
|
||||
const confirmButton = screen.getByText('app.newApp.Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -899,18 +849,18 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const confirmButton = screen.getByText('newApp.Confirm')
|
||||
const confirmButton = screen.getByText('app.newApp.Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -941,18 +891,18 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const confirmButton = screen.getByText('newApp.Confirm')
|
||||
const confirmButton = screen.getByText('app.newApp.Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -983,18 +933,18 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const confirmButton = screen.getByText('newApp.Confirm')
|
||||
const confirmButton = screen.getByText('app.newApp.Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -1025,26 +975,23 @@ describe('UpdateDSLModal', () => {
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
// Flush microtasks scheduled by the FileReader mock (which uses queueMicrotask)
|
||||
await new Promise<void>(resolve => queueMicrotask(resolve))
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(importButton)
|
||||
// Flush the promise resolution from mockImportDSL
|
||||
await Promise.resolve()
|
||||
// Advance past the 300ms setTimeout in the component
|
||||
await vi.advanceTimersByTimeAsync(350)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
|
||||
const confirmButton = screen.getByText('newApp.Confirm')
|
||||
const confirmButton = screen.getByText('app.newApp.Confirm')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
@ -1070,25 +1017,21 @@ describe('UpdateDSLModal', () => {
|
||||
fireEvent.change(fileInput, { target: { files: [file] } })
|
||||
|
||||
await waitFor(() => {
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
expect(importButton).not.toBeDisabled()
|
||||
})
|
||||
|
||||
const importButton = screen.getByText('common.overwriteAndImport')
|
||||
const importButton = screen.getByText('workflow.common.overwriteAndImport')
|
||||
fireEvent.click(importButton)
|
||||
|
||||
// Should show error modal even with undefined versions
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
expect(screen.getByText('app.newApp.appCreateDSLErrorTitle')).toBeInTheDocument()
|
||||
}, { timeout: 1000 })
|
||||
})
|
||||
|
||||
it('should not call importDSLConfirm when importId is not set', async () => {
|
||||
// Render without triggering PENDING status first
|
||||
render(<UpdateDSLModal {...defaultProps} />)
|
||||
|
||||
// importId is not set, so confirm should not be called
|
||||
// This is hard to test directly, but we can verify by checking the confirm flow
|
||||
expect(mockImportDSLConfirm).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -1,6 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import VersionMismatchModal from './version-mismatch-modal'
|
||||
import VersionMismatchModal from '../version-mismatch-modal'
|
||||
|
||||
describe('VersionMismatchModal', () => {
|
||||
const mockOnClose = vi.fn()
|
||||
@ -1,15 +1,9 @@
|
||||
import type { ParentChildChunk } from './types'
|
||||
import type { ParentChildChunk } from '../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
|
||||
import ChunkCard from './chunk-card'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: Record<string, unknown>) => `${key}${opts?.count !== undefined ? `:${opts.count}` : ''}`,
|
||||
}),
|
||||
}))
|
||||
import ChunkCard from '../chunk-card'
|
||||
|
||||
vi.mock('@/app/components/datasets/documents/detail/completed/common/dot', () => ({
|
||||
default: () => <span data-testid="dot" />,
|
||||
@ -52,13 +46,13 @@ vi.mock('@/utils/format', () => ({
|
||||
formatNumber: (n: number) => String(n),
|
||||
}))
|
||||
|
||||
vi.mock('./q-a-item', () => ({
|
||||
vi.mock('../q-a-item', () => ({
|
||||
default: ({ type, text }: { type: string, text: string }) => (
|
||||
<span data-testid={`qa-${type}`}>{text}</span>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./types', () => ({
|
||||
vi.mock('../types', () => ({
|
||||
QAItemType: {
|
||||
Question: 'question',
|
||||
Answer: 'answer',
|
||||
@ -1,14 +1,10 @@
|
||||
import type { GeneralChunks, ParentChildChunk, ParentChildChunks, QAChunk, QAChunks } from './types'
|
||||
import type { GeneralChunks, ParentChildChunk, ParentChildChunks, QAChunk, QAChunks } from '../types'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import ChunkCard from './chunk-card'
|
||||
import { ChunkCardList } from './index'
|
||||
import QAItem from './q-a-item'
|
||||
import { QAItemType } from './types'
|
||||
|
||||
// =============================================================================
|
||||
// Test Data Factories
|
||||
// =============================================================================
|
||||
import ChunkCard from '../chunk-card'
|
||||
import { ChunkCardList } from '../index'
|
||||
import QAItem from '../q-a-item'
|
||||
import { QAItemType } from '../types'
|
||||
|
||||
const createGeneralChunks = (overrides: GeneralChunks = []): GeneralChunks => {
|
||||
if (overrides.length > 0)
|
||||
@ -56,99 +52,71 @@ const createQAChunks = (overrides: Partial<QAChunks> = {}): QAChunks => ({
|
||||
...overrides,
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// QAItem Component Tests
|
||||
// =============================================================================
|
||||
|
||||
describe('QAItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Tests for basic rendering of QAItem component
|
||||
describe('Rendering', () => {
|
||||
it('should render question type with Q prefix', () => {
|
||||
// Arrange & Act
|
||||
render(<QAItem type={QAItemType.Question} text="What is this?" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('What is this?')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render answer type with A prefix', () => {
|
||||
// Arrange & Act
|
||||
render(<QAItem type={QAItemType.Answer} text="This is the answer." />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
expect(screen.getByText('This is the answer.')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for different prop variations
|
||||
describe('Props', () => {
|
||||
it('should render with empty text', () => {
|
||||
// Arrange & Act
|
||||
render(<QAItem type={QAItemType.Question} text="" />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with long text content', () => {
|
||||
// Arrange
|
||||
const longText = 'A'.repeat(1000)
|
||||
|
||||
// Act
|
||||
render(<QAItem type={QAItemType.Answer} text={longText} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(longText)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with special characters in text', () => {
|
||||
// Arrange
|
||||
const specialText = '<script>alert("xss")</script> & "quotes" \'apostrophe\''
|
||||
|
||||
// Act
|
||||
render(<QAItem type={QAItemType.Question} text={specialText} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(specialText)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for memoization behavior
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized with React.memo', () => {
|
||||
// Arrange & Act
|
||||
const { rerender } = render(<QAItem type={QAItemType.Question} text="Test" />)
|
||||
|
||||
// Assert - component should render consistently
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('Test')).toBeInTheDocument()
|
||||
|
||||
// Rerender with same props - should not cause issues
|
||||
rerender(<QAItem type={QAItemType.Question} text="Test" />)
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// ChunkCard Component Tests
|
||||
// =============================================================================
|
||||
|
||||
describe('ChunkCard', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Tests for basic rendering with different chunk types
|
||||
describe('Rendering', () => {
|
||||
it('should render text chunk type correctly', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -158,19 +126,16 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('This is the first chunk of text content.')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Chunk-01/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render QA chunk type with question and answer', () => {
|
||||
// Arrange
|
||||
const qaContent: QAChunk = {
|
||||
question: 'What is React?',
|
||||
answer: 'React is a JavaScript library.',
|
||||
}
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -180,7 +145,6 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('What is React?')).toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
@ -188,10 +152,8 @@ describe('ChunkCard', () => {
|
||||
})
|
||||
|
||||
it('should render parent-child chunk type with child contents', () => {
|
||||
// Arrange
|
||||
const childContents = ['Child 1 content', 'Child 2 content']
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -202,7 +164,6 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Child 1 content')).toBeInTheDocument()
|
||||
expect(screen.getByText('Child 2 content')).toBeInTheDocument()
|
||||
expect(screen.getByText('C-1')).toBeInTheDocument()
|
||||
@ -210,10 +171,8 @@ describe('ChunkCard', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for parent mode variations
|
||||
describe('Parent Mode Variations', () => {
|
||||
it('should show Parent-Chunk label prefix for paragraph mode', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -224,12 +183,10 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/Parent-Chunk-01/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide segment index tag for full-doc mode', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -240,13 +197,11 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should not show Chunk or Parent-Chunk label
|
||||
expect(screen.queryByText(/Chunk/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Parent-Chunk/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show Chunk label prefix for text mode', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -256,15 +211,12 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/Chunk-05/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for word count display
|
||||
describe('Word Count Display', () => {
|
||||
it('should display formatted word count', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -274,12 +226,10 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - formatNumber(1234) returns '1,234'
|
||||
expect(screen.getByText(/1,234/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display word count with character translation key', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -289,12 +239,10 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - translation key is returned as-is by mock
|
||||
expect(screen.getByText(/100\s+(?:\S.*)?characters/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not display word count info for full-doc mode', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -305,15 +253,12 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - the header with word count should be hidden
|
||||
expect(screen.queryByText(/500/)).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for position ID variations
|
||||
describe('Position ID', () => {
|
||||
it('should handle numeric position ID', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -323,12 +268,10 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/Chunk-42/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle string position ID', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -338,12 +281,10 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/Chunk-99/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should pad single digit position ID', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -353,15 +294,12 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(/Chunk-03/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for memoization dependencies
|
||||
describe('Memoization', () => {
|
||||
it('should update isFullDoc memo when parentMode changes', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -372,10 +310,8 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - paragraph mode shows label
|
||||
expect(screen.getByText(/Parent-Chunk/)).toBeInTheDocument()
|
||||
|
||||
// Act - change to full-doc
|
||||
rerender(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -386,12 +322,10 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - full-doc mode hides label
|
||||
expect(screen.queryByText(/Parent-Chunk/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update contentElement memo when content changes', () => {
|
||||
// Arrange
|
||||
const initialContent = { content: 'Initial content' }
|
||||
const updatedContent = { content: 'Updated content' }
|
||||
|
||||
@ -404,10 +338,8 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Initial content')).toBeInTheDocument()
|
||||
|
||||
// Act
|
||||
rerender(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -417,13 +349,11 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Updated content')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Initial content')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update contentElement memo when chunkType changes', () => {
|
||||
// Arrange
|
||||
const textContent = { content: 'Text content' }
|
||||
const { rerender } = render(
|
||||
<ChunkCard
|
||||
@ -434,10 +364,8 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Text content')).toBeInTheDocument()
|
||||
|
||||
// Act - change to QA type
|
||||
const qaContent: QAChunk = { question: 'Q?', answer: 'A.' }
|
||||
rerender(
|
||||
<ChunkCard
|
||||
@ -448,16 +376,13 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('Q?')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty child contents array', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -468,15 +393,12 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should render without errors
|
||||
expect(screen.getByText(/Parent-Chunk-01/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle QA chunk with empty strings', () => {
|
||||
// Arrange
|
||||
const emptyQA: QAChunk = { question: '', answer: '' }
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -486,17 +408,14 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('A')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle very long content', () => {
|
||||
// Arrange
|
||||
const longContent = 'A'.repeat(10000)
|
||||
const longContentChunk = { content: longContent }
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -506,12 +425,10 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(longContent)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle zero word count', () => {
|
||||
// Arrange & Act
|
||||
render(
|
||||
<ChunkCard
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -521,28 +438,20 @@ describe('ChunkCard', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - formatNumber returns falsy for 0, so it shows 0
|
||||
expect(screen.getByText(/0\s+(?:\S.*)?characters/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// ChunkCardList Component Tests
|
||||
// =============================================================================
|
||||
|
||||
describe('ChunkCardList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Tests for rendering with different chunk types
|
||||
describe('Rendering', () => {
|
||||
it('should render text chunks correctly', () => {
|
||||
// Arrange
|
||||
const chunks = createGeneralChunks()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -550,17 +459,14 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText(chunks[0].content)).toBeInTheDocument()
|
||||
expect(screen.getByText(chunks[1].content)).toBeInTheDocument()
|
||||
expect(screen.getByText(chunks[2].content)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render parent-child chunks correctly', () => {
|
||||
// Arrange
|
||||
const chunks = createParentChildChunks()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -569,17 +475,14 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should render child contents from parent-child chunks
|
||||
expect(screen.getByText('Child content 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Child content 2')).toBeInTheDocument()
|
||||
expect(screen.getByText('Another child 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render QA chunks correctly', () => {
|
||||
// Arrange
|
||||
const chunks = createQAChunks()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -587,7 +490,6 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('What is the answer to life?')).toBeInTheDocument()
|
||||
expect(screen.getByText('The answer is 42.')).toBeInTheDocument()
|
||||
expect(screen.getByText('How does this work?')).toBeInTheDocument()
|
||||
@ -595,16 +497,13 @@ describe('ChunkCardList', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for chunkList memoization
|
||||
describe('Memoization - chunkList', () => {
|
||||
it('should extract chunks from GeneralChunks for text mode', () => {
|
||||
// Arrange
|
||||
const chunks: GeneralChunks = [
|
||||
{ content: 'Chunk 1' },
|
||||
{ content: 'Chunk 2' },
|
||||
]
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -612,20 +511,17 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Chunk 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Chunk 2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should extract parent_child_chunks from ParentChildChunks for parentChild mode', () => {
|
||||
// Arrange
|
||||
const chunks = createParentChildChunks({
|
||||
parent_child_chunks: [
|
||||
createParentChildChunk({ child_contents: ['Specific child'] }),
|
||||
],
|
||||
})
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -634,19 +530,16 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Specific child')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should extract qa_chunks from QAChunks for qa mode', () => {
|
||||
// Arrange
|
||||
const chunks: QAChunks = {
|
||||
qa_chunks: [
|
||||
{ question: 'Specific Q', answer: 'Specific A' },
|
||||
],
|
||||
}
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -654,13 +547,11 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Specific Q')).toBeInTheDocument()
|
||||
expect(screen.getByText('Specific A')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should update chunkList when chunkInfo changes', () => {
|
||||
// Arrange
|
||||
const initialChunks = createGeneralChunks([{ content: 'Initial chunk' }])
|
||||
|
||||
const { rerender } = render(
|
||||
@ -670,10 +561,8 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert initial state
|
||||
expect(screen.getByText('Initial chunk')).toBeInTheDocument()
|
||||
|
||||
// Act - update chunks
|
||||
const updatedChunks = createGeneralChunks([{ content: 'Updated chunk' }])
|
||||
rerender(
|
||||
<ChunkCardList
|
||||
@ -682,19 +571,15 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert updated state
|
||||
expect(screen.getByText('Updated chunk')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Initial chunk')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for getWordCount function
|
||||
describe('Word Count Calculation', () => {
|
||||
it('should calculate word count for text chunks using string length', () => {
|
||||
// Arrange - "Hello" has 5 characters
|
||||
const chunks = createGeneralChunks([{ content: 'Hello' }])
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -702,12 +587,10 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - word count should be 5 (string length)
|
||||
expect(screen.getByText(/5\s+(?:\S.*)?characters/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should calculate word count for parent-child chunks using parent_content length', () => {
|
||||
// Arrange - parent_content length determines word count
|
||||
const chunks = createParentChildChunks({
|
||||
parent_child_chunks: [
|
||||
createParentChildChunk({
|
||||
@ -717,7 +600,6 @@ describe('ChunkCardList', () => {
|
||||
],
|
||||
})
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -726,19 +608,16 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - word count should be 6 (parent_content length)
|
||||
expect(screen.getByText(/6\s+(?:\S.*)?characters/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should calculate word count for QA chunks using question + answer length', () => {
|
||||
// Arrange - "Hi" (2) + "Bye" (3) = 5
|
||||
const chunks: QAChunks = {
|
||||
qa_chunks: [
|
||||
{ question: 'Hi', answer: 'Bye' },
|
||||
],
|
||||
}
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -746,22 +625,18 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - word count should be 5 (question.length + answer.length)
|
||||
expect(screen.getByText(/5\s+(?:\S.*)?characters/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for position ID assignment
|
||||
describe('Position ID', () => {
|
||||
it('should assign 1-based position IDs to chunks', () => {
|
||||
// Arrange
|
||||
const chunks = createGeneralChunks([
|
||||
{ content: 'First' },
|
||||
{ content: 'Second' },
|
||||
{ content: 'Third' },
|
||||
])
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -769,20 +644,16 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - position IDs should be 1, 2, 3
|
||||
expect(screen.getByText(/Chunk-01/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Chunk-02/)).toBeInTheDocument()
|
||||
expect(screen.getByText(/Chunk-03/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for className prop
|
||||
describe('Custom className', () => {
|
||||
it('should apply custom className to container', () => {
|
||||
// Arrange
|
||||
const chunks = createGeneralChunks([{ content: 'Test' }])
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -791,15 +662,12 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toHaveClass('custom-class')
|
||||
})
|
||||
|
||||
it('should merge custom className with default classes', () => {
|
||||
// Arrange
|
||||
const chunks = createGeneralChunks([{ content: 'Test' }])
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -808,7 +676,6 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should have both default and custom classes
|
||||
expect(container.firstChild).toHaveClass('flex')
|
||||
expect(container.firstChild).toHaveClass('w-full')
|
||||
expect(container.firstChild).toHaveClass('flex-col')
|
||||
@ -816,10 +683,8 @@ describe('ChunkCardList', () => {
|
||||
})
|
||||
|
||||
it('should render without className prop', () => {
|
||||
// Arrange
|
||||
const chunks = createGeneralChunks([{ content: 'Test' }])
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -827,19 +692,15 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should have default classes
|
||||
expect(container.firstChild).toHaveClass('flex')
|
||||
expect(container.firstChild).toHaveClass('w-full')
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for parentMode prop
|
||||
describe('Parent Mode', () => {
|
||||
it('should pass parentMode to ChunkCard for parent-child type', () => {
|
||||
// Arrange
|
||||
const chunks = createParentChildChunks()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -848,15 +709,12 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - paragraph mode shows Parent-Chunk label
|
||||
expect(screen.getAllByText(/Parent-Chunk/).length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should handle full-doc parentMode', () => {
|
||||
// Arrange
|
||||
const chunks = createParentChildChunks()
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -865,16 +723,13 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - full-doc mode hides chunk labels
|
||||
expect(screen.queryByText(/Parent-Chunk/)).not.toBeInTheDocument()
|
||||
expect(screen.queryByText(/Chunk-/)).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not use parentMode for text type', () => {
|
||||
// Arrange
|
||||
const chunks = createGeneralChunks([{ content: 'Text' }])
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -883,18 +738,14 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should show Chunk label, not affected by parentMode
|
||||
expect(screen.getByText(/Chunk-01/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty GeneralChunks array', () => {
|
||||
// Arrange
|
||||
const chunks: GeneralChunks = []
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -902,19 +753,16 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - should render empty container
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(container.firstChild?.childNodes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle empty ParentChildChunks', () => {
|
||||
// Arrange
|
||||
const chunks: ParentChildChunks = {
|
||||
parent_child_chunks: [],
|
||||
parent_mode: 'paragraph',
|
||||
}
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -923,18 +771,15 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(container.firstChild?.childNodes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle empty QAChunks', () => {
|
||||
// Arrange
|
||||
const chunks: QAChunks = {
|
||||
qa_chunks: [],
|
||||
}
|
||||
|
||||
// Act
|
||||
const { container } = render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -942,16 +787,13 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(container.firstChild).toBeInTheDocument()
|
||||
expect(container.firstChild?.childNodes.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should handle single item in chunks', () => {
|
||||
// Arrange
|
||||
const chunks = createGeneralChunks([{ content: 'Single chunk' }])
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -959,16 +801,13 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Single chunk')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Chunk-01/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle large number of chunks', () => {
|
||||
// Arrange
|
||||
const chunks = Array.from({ length: 100 }, (_, i) => ({ content: `Chunk number ${i + 1}` }))
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -976,23 +815,19 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('Chunk number 1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Chunk number 100')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Chunk-100/)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for key uniqueness
|
||||
describe('Key Generation', () => {
|
||||
it('should generate unique keys for chunks', () => {
|
||||
// Arrange - chunks with same content
|
||||
const chunks = createGeneralChunks([
|
||||
{ content: 'Same content' },
|
||||
{ content: 'Same content' },
|
||||
{ content: 'Same content' },
|
||||
])
|
||||
// Act
|
||||
const { container } = render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -1000,33 +835,25 @@ describe('ChunkCardList', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert - all three should render (keys are based on chunkType-index)
|
||||
const chunkCards = container.querySelectorAll('.bg-components-panel-bg')
|
||||
expect(chunkCards.length).toBe(3)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Integration Tests
|
||||
// =============================================================================
|
||||
|
||||
describe('ChunkCardList Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Tests for complete workflow scenarios
|
||||
describe('Complete Workflows', () => {
|
||||
it('should render complete text chunking workflow', () => {
|
||||
// Arrange
|
||||
const textChunks = createGeneralChunks([
|
||||
{ content: 'First paragraph of the document.' },
|
||||
{ content: 'Second paragraph with more information.' },
|
||||
{ content: 'Final paragraph concluding the content.' },
|
||||
])
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.text}
|
||||
@ -1034,10 +861,8 @@ describe('ChunkCardList Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('First paragraph of the document.')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Chunk-01/)).toBeInTheDocument()
|
||||
// "First paragraph of the document." = 32 characters
|
||||
expect(screen.getByText(/32\s+(?:\S.*)?characters/)).toBeInTheDocument()
|
||||
|
||||
expect(screen.getByText('Second paragraph with more information.')).toBeInTheDocument()
|
||||
@ -1048,7 +873,6 @@ describe('ChunkCardList Integration', () => {
|
||||
})
|
||||
|
||||
it('should render complete parent-child chunking workflow', () => {
|
||||
// Arrange
|
||||
const parentChildChunks = createParentChildChunks({
|
||||
parent_child_chunks: [
|
||||
{
|
||||
@ -1062,7 +886,6 @@ describe('ChunkCardList Integration', () => {
|
||||
],
|
||||
})
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -1071,7 +894,6 @@ describe('ChunkCardList Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('React components are building blocks.')).toBeInTheDocument()
|
||||
expect(screen.getByText('Lifecycle methods control component behavior.')).toBeInTheDocument()
|
||||
expect(screen.getByText('C-1')).toBeInTheDocument()
|
||||
@ -1080,7 +902,6 @@ describe('ChunkCardList Integration', () => {
|
||||
})
|
||||
|
||||
it('should render complete QA chunking workflow', () => {
|
||||
// Arrange
|
||||
const qaChunks = createQAChunks({
|
||||
qa_chunks: [
|
||||
{
|
||||
@ -1094,7 +915,6 @@ describe('ChunkCardList Integration', () => {
|
||||
],
|
||||
})
|
||||
|
||||
// Act
|
||||
render(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -1102,7 +922,6 @@ describe('ChunkCardList Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert
|
||||
const qLabels = screen.getAllByText('Q')
|
||||
const aLabels = screen.getAllByText('A')
|
||||
expect(qLabels.length).toBe(2)
|
||||
@ -1115,10 +934,8 @@ describe('ChunkCardList Integration', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Tests for type switching scenarios
|
||||
describe('Type Switching', () => {
|
||||
it('should handle switching from text to QA type', () => {
|
||||
// Arrange
|
||||
const textChunks = createGeneralChunks([{ content: 'Text content' }])
|
||||
const qaChunks = createQAChunks()
|
||||
|
||||
@ -1129,10 +946,8 @@ describe('ChunkCardList Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert initial text state
|
||||
expect(screen.getByText('Text content')).toBeInTheDocument()
|
||||
|
||||
// Act - switch to QA
|
||||
rerender(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.qa}
|
||||
@ -1140,13 +955,11 @@ describe('ChunkCardList Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert QA state
|
||||
expect(screen.queryByText('Text content')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('What is the answer to life?')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle switching from text to parent-child type', () => {
|
||||
// Arrange
|
||||
const textChunks = createGeneralChunks([{ content: 'Simple text' }])
|
||||
const parentChildChunks = createParentChildChunks()
|
||||
|
||||
@ -1157,11 +970,9 @@ describe('ChunkCardList Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert initial state
|
||||
expect(screen.getByText('Simple text')).toBeInTheDocument()
|
||||
expect(screen.getByText(/Chunk-01/)).toBeInTheDocument()
|
||||
|
||||
// Act - switch to parent-child
|
||||
rerender(
|
||||
<ChunkCardList
|
||||
chunkType={ChunkingMode.parentChild}
|
||||
@ -1170,9 +981,7 @@ describe('ChunkCardList Integration', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Assert parent-child state
|
||||
expect(screen.queryByText('Simple text')).not.toBeInTheDocument()
|
||||
// Multiple Parent-Chunk elements exist, so use getAllByText
|
||||
expect(screen.getAllByText(/Parent-Chunk/).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@ -1,13 +1,8 @@
|
||||
import type { PanelProps } from '@/app/components/workflow/panel'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import RagPipelinePanel from './index'
|
||||
import RagPipelinePanel from '../index'
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Dependencies
|
||||
// ============================================================================
|
||||
|
||||
// Mock reactflow to avoid zustand provider error
|
||||
vi.mock('reactflow', () => ({
|
||||
useNodes: () => [],
|
||||
useStoreApi: () => ({
|
||||
@ -26,20 +21,12 @@ vi.mock('reactflow', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Use vi.hoisted to create variables that can be used in vi.mock
|
||||
const { dynamicMocks, mockInputFieldEditorProps } = vi.hoisted(() => {
|
||||
let counter = 0
|
||||
const mockInputFieldEditorProps = vi.fn()
|
||||
|
||||
const createMockComponent = () => {
|
||||
const index = counter++
|
||||
// Order matches the imports in index.tsx:
|
||||
// 0: Record
|
||||
// 1: TestRunPanel
|
||||
// 2: InputFieldPanel
|
||||
// 3: InputFieldEditorPanel
|
||||
// 4: PreviewPanel
|
||||
// 5: GlobalVariablePanel
|
||||
switch (index) {
|
||||
case 0:
|
||||
return () => <div data-testid="record-panel">Record Panel</div>
|
||||
@ -69,14 +56,12 @@ const { dynamicMocks, mockInputFieldEditorProps } = vi.hoisted(() => {
|
||||
return { dynamicMocks: { createMockComponent }, mockInputFieldEditorProps }
|
||||
})
|
||||
|
||||
// Mock next/dynamic
|
||||
vi.mock('next/dynamic', () => ({
|
||||
default: (_loader: () => Promise<{ default: React.ComponentType }>, _options?: Record<string, unknown>) => {
|
||||
return dynamicMocks.createMockComponent()
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
let mockHistoryWorkflowData: Record<string, unknown> | null = null
|
||||
let mockShowDebugAndPreviewPanel = false
|
||||
let mockShowGlobalVariablePanel = false
|
||||
@ -138,7 +123,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Panel component to capture props and render children
|
||||
let capturedPanelProps: PanelProps | null = null
|
||||
vi.mock('@/app/components/workflow/panel', () => ({
|
||||
default: (props: PanelProps) => {
|
||||
@ -152,10 +136,6 @@ vi.mock('@/app/components/workflow/panel', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
type SetupMockOptions = {
|
||||
historyWorkflowData?: Record<string, unknown> | null
|
||||
showDebugAndPreviewPanel?: boolean
|
||||
@ -177,35 +157,24 @@ const setupMocks = (options?: SetupMockOptions) => {
|
||||
capturedPanelProps = null
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RagPipelinePanel Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('RagPipelinePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should render Panel component with correct structure', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('panel-left')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('panel-right')).toBeInTheDocument()
|
||||
@ -213,13 +182,10 @@ describe('RagPipelinePanel', () => {
|
||||
})
|
||||
|
||||
it('should pass versionHistoryPanelProps to Panel', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'my-pipeline-456' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toBeDefined()
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
@ -229,18 +195,12 @@ describe('RagPipelinePanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests - versionHistoryPanelProps
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization - versionHistoryPanelProps', () => {
|
||||
it('should compute correct getVersionListUrl based on pipelineId', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-abc' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines/pipeline-abc/workflows',
|
||||
@ -249,13 +209,10 @@ describe('RagPipelinePanel', () => {
|
||||
})
|
||||
|
||||
it('should compute correct deleteVersionUrl function', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-xyz' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const deleteUrl = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-1')
|
||||
expect(deleteUrl).toBe('/rag/pipelines/pipeline-xyz/workflows/version-1')
|
||||
@ -263,13 +220,10 @@ describe('RagPipelinePanel', () => {
|
||||
})
|
||||
|
||||
it('should compute correct updateVersionUrl function', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-def' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const updateUrl = capturedPanelProps?.versionHistoryPanelProps?.updateVersionUrl?.('version-2')
|
||||
expect(updateUrl).toBe('/rag/pipelines/pipeline-def/workflows/version-2')
|
||||
@ -277,63 +231,46 @@ describe('RagPipelinePanel', () => {
|
||||
})
|
||||
|
||||
it('should set latestVersionId to empty string', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.latestVersionId).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests - panelProps
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization - panelProps', () => {
|
||||
it('should pass components.left to Panel', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.components?.left).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass components.right to Panel', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.components?.right).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass versionHistoryPanelProps to panelProps', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Component Memoization Tests (React.memo)
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Component Memoization', () => {
|
||||
it('should be wrapped with React.memo', async () => {
|
||||
// The component should not break when re-rendered
|
||||
const { rerender } = render(<RagPipelinePanel />)
|
||||
|
||||
// Act - rerender without prop changes
|
||||
rerender(<RagPipelinePanel />)
|
||||
|
||||
// Assert - component should still render correctly
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
|
||||
})
|
||||
@ -341,138 +278,98 @@ describe('RagPipelinePanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// RagPipelinePanelOnRight Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('RagPipelinePanelOnRight', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - Record Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Record Panel Conditional Rendering', () => {
|
||||
it('should render Record panel when historyWorkflowData exists', async () => {
|
||||
// Arrange
|
||||
setupMocks({ historyWorkflowData: { id: 'history-1' } })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Record panel when historyWorkflowData is null', async () => {
|
||||
// Arrange
|
||||
setupMocks({ historyWorkflowData: null })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Record panel when historyWorkflowData is undefined', async () => {
|
||||
// Arrange
|
||||
setupMocks({ historyWorkflowData: undefined })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - TestRun Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('TestRun Panel Conditional Rendering', () => {
|
||||
it('should render TestRun panel when showDebugAndPreviewPanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showDebugAndPreviewPanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render TestRun panel when showDebugAndPreviewPanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showDebugAndPreviewPanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('test-run-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - GlobalVariable Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('GlobalVariable Panel Conditional Rendering', () => {
|
||||
it('should render GlobalVariable panel when showGlobalVariablePanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showGlobalVariablePanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('global-variable-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render GlobalVariable panel when showGlobalVariablePanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showGlobalVariablePanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('global-variable-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Multiple Panels Rendering
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Multiple Panels Rendering', () => {
|
||||
it('should render all right panels when all conditions are true', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'history-1' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
showGlobalVariablePanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
@ -481,17 +378,14 @@ describe('RagPipelinePanelOnRight', () => {
|
||||
})
|
||||
|
||||
it('should render no right panels when all conditions are false', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: null,
|
||||
showDebugAndPreviewPanel: false,
|
||||
showGlobalVariablePanel: false,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('record-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('test-run-panel')).not.toBeInTheDocument()
|
||||
@ -500,17 +394,14 @@ describe('RagPipelinePanelOnRight', () => {
|
||||
})
|
||||
|
||||
it('should render only Record and TestRun panels', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'history-1' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
showGlobalVariablePanel: false,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
@ -520,53 +411,36 @@ describe('RagPipelinePanelOnRight', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// RagPipelinePanelOnLeft Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('RagPipelinePanelOnLeft', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - Preview Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Preview Panel Conditional Rendering', () => {
|
||||
it('should render Preview panel when showInputFieldPreviewPanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPreviewPanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render Preview panel when showInputFieldPreviewPanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPreviewPanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('preview-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - InputFieldEditor Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('InputFieldEditor Panel Conditional Rendering', () => {
|
||||
it('should render InputFieldEditor panel when inputFieldEditPanelProps is provided', async () => {
|
||||
// Arrange
|
||||
const editProps = {
|
||||
onClose: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
@ -574,30 +448,24 @@ describe('RagPipelinePanelOnLeft', () => {
|
||||
}
|
||||
setupMocks({ inputFieldEditPanelProps: editProps })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render InputFieldEditor panel when inputFieldEditPanelProps is null', async () => {
|
||||
// Arrange
|
||||
setupMocks({ inputFieldEditPanelProps: null })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should pass props to InputFieldEditor panel', async () => {
|
||||
// Arrange
|
||||
const editProps = {
|
||||
onClose: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
@ -605,10 +473,8 @@ describe('RagPipelinePanelOnLeft', () => {
|
||||
}
|
||||
setupMocks({ inputFieldEditPanelProps: editProps })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockInputFieldEditorProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -621,53 +487,38 @@ describe('RagPipelinePanelOnLeft', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Conditional Rendering - InputField Panel
|
||||
// -------------------------------------------------------------------------
|
||||
describe('InputField Panel Conditional Rendering', () => {
|
||||
it('should render InputField panel when showInputFieldPanel is true', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPanel: true })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('input-field-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should not render InputField panel when showInputFieldPanel is false', async () => {
|
||||
// Arrange
|
||||
setupMocks({ showInputFieldPanel: false })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('input-field-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Multiple Panels Rendering
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Multiple Left Panels Rendering', () => {
|
||||
it('should render all left panels when all conditions are true', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
showInputFieldPreviewPanel: true,
|
||||
inputFieldEditPanelProps: { onClose: vi.fn(), onSubmit: vi.fn() },
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('input-field-editor-panel')).toBeInTheDocument()
|
||||
@ -676,17 +527,14 @@ describe('RagPipelinePanelOnLeft', () => {
|
||||
})
|
||||
|
||||
it('should render no left panels when all conditions are false', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
showInputFieldPreviewPanel: false,
|
||||
inputFieldEditPanelProps: null,
|
||||
showInputFieldPanel: false,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId('preview-panel')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
|
||||
@ -695,17 +543,14 @@ describe('RagPipelinePanelOnLeft', () => {
|
||||
})
|
||||
|
||||
it('should render only Preview and InputField panels', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
showInputFieldPreviewPanel: true,
|
||||
inputFieldEditPanelProps: null,
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('preview-panel')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('input-field-editor-panel')).not.toBeInTheDocument()
|
||||
@ -715,28 +560,18 @@ describe('RagPipelinePanelOnLeft', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Empty/Undefined Values
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Empty/Undefined Values', () => {
|
||||
it('should handle empty pipelineId gracefully', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: '' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines//workflows',
|
||||
@ -745,13 +580,10 @@ describe('Edge Cases', () => {
|
||||
})
|
||||
|
||||
it('should handle special characters in pipelineId', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'pipeline-with-special_chars.123' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps?.getVersionListUrl).toBe(
|
||||
'/rag/pipelines/pipeline-with-special_chars.123/workflows',
|
||||
@ -760,12 +592,8 @@ describe('Edge Cases', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Props Spreading Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Props Spreading', () => {
|
||||
it('should correctly spread inputFieldEditPanelProps to editor component', async () => {
|
||||
// Arrange
|
||||
const customProps = {
|
||||
onClose: vi.fn(),
|
||||
onSubmit: vi.fn(),
|
||||
@ -778,10 +606,8 @@ describe('Edge Cases', () => {
|
||||
}
|
||||
setupMocks({ inputFieldEditPanelProps: customProps })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockInputFieldEditorProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@ -792,12 +618,8 @@ describe('Edge Cases', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// State Combinations
|
||||
// -------------------------------------------------------------------------
|
||||
describe('State Combinations', () => {
|
||||
it('should handle all panels visible simultaneously', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'h1' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
@ -807,10 +629,8 @@ describe('Edge Cases', () => {
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert - All panels should be visible
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
@ -823,10 +643,6 @@ describe('Edge Cases', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// URL Generator Functions Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('URL Generator Functions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -834,13 +650,10 @@ describe('URL Generator Functions', () => {
|
||||
})
|
||||
|
||||
it('should return consistent URLs for same versionId', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'stable-pipeline' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const deleteUrl1 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-x')
|
||||
const deleteUrl2 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-x')
|
||||
@ -849,13 +662,10 @@ describe('URL Generator Functions', () => {
|
||||
})
|
||||
|
||||
it('should return different URLs for different versionIds', async () => {
|
||||
// Arrange
|
||||
setupMocks({ pipelineId: 'stable-pipeline' })
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
const deleteUrl1 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-1')
|
||||
const deleteUrl2 = capturedPanelProps?.versionHistoryPanelProps?.deleteVersionUrl?.('version-2')
|
||||
@ -866,10 +676,6 @@ describe('URL Generator Functions', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Type Safety Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Type Safety', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -877,10 +683,8 @@ describe('Type Safety', () => {
|
||||
})
|
||||
|
||||
it('should pass correct PanelProps structure', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert - Check structure matches PanelProps
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps).toHaveProperty('components')
|
||||
expect(capturedPanelProps).toHaveProperty('versionHistoryPanelProps')
|
||||
@ -890,10 +694,8 @@ describe('Type Safety', () => {
|
||||
})
|
||||
|
||||
it('should pass correct versionHistoryPanelProps structure', async () => {
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('getVersionListUrl')
|
||||
expect(capturedPanelProps?.versionHistoryPanelProps).toHaveProperty('deleteVersionUrl')
|
||||
@ -903,10 +705,6 @@ describe('Type Safety', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Performance Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Performance', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -914,24 +712,17 @@ describe('Performance', () => {
|
||||
})
|
||||
|
||||
it('should handle multiple rerenders without issues', async () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<RagPipelinePanel />)
|
||||
|
||||
// Act - Multiple rerenders
|
||||
for (let i = 0; i < 10; i++)
|
||||
rerender(<RagPipelinePanel />)
|
||||
|
||||
// Assert - Component should still work
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('workflow-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Integration Tests', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -939,28 +730,23 @@ describe('Integration Tests', () => {
|
||||
})
|
||||
|
||||
it('should pass correct components to Panel', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'h1' },
|
||||
showInputFieldPanel: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(capturedPanelProps?.components?.left).toBeDefined()
|
||||
expect(capturedPanelProps?.components?.right).toBeDefined()
|
||||
|
||||
// Check that the components are React elements
|
||||
expect(React.isValidElement(capturedPanelProps?.components?.left)).toBe(true)
|
||||
expect(React.isValidElement(capturedPanelProps?.components?.right)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('should correctly consume all store selectors', async () => {
|
||||
// Arrange
|
||||
setupMocks({
|
||||
historyWorkflowData: { id: 'test-history' },
|
||||
showDebugAndPreviewPanel: true,
|
||||
@ -971,10 +757,8 @@ describe('Integration Tests', () => {
|
||||
pipelineId: 'integration-test-pipeline',
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<RagPipelinePanel />)
|
||||
|
||||
// Assert - All store-dependent rendering should work
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('record-panel')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('test-run-panel')).toBeInTheDocument()
|
||||
@ -1,6 +1,6 @@
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import FooterTip from './footer-tip'
|
||||
import FooterTip from '../footer-tip'
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
@ -45,7 +45,6 @@ describe('FooterTip', () => {
|
||||
it('should render the drag icon', () => {
|
||||
const { container } = render(<FooterTip />)
|
||||
|
||||
// The RiDragDropLine icon should be rendered
|
||||
const icon = container.querySelector('.size-4')
|
||||
expect(icon).toBeInTheDocument()
|
||||
})
|
||||
@ -1,8 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useFloatingRight } from './hooks'
|
||||
import { useFloatingRight } from '../hooks'
|
||||
|
||||
// Mock reactflow
|
||||
const mockGetNodes = vi.fn()
|
||||
vi.mock('reactflow', () => ({
|
||||
useStore: (selector: (s: { getNodes: () => { id: string, data: { selected: boolean } }[] }) => unknown) => {
|
||||
@ -10,12 +9,10 @@ vi.mock('reactflow', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock zustand/react/shallow
|
||||
vi.mock('zustand/react/shallow', () => ({
|
||||
useShallow: (fn: (...args: unknown[]) => unknown) => fn,
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
let mockNodePanelWidth = 400
|
||||
let mockWorkflowCanvasWidth: number | undefined = 1200
|
||||
let mockOtherPanelWidth = 0
|
||||
@ -67,8 +64,6 @@ describe('useFloatingRight', () => {
|
||||
|
||||
const { result } = renderHook(() => useFloatingRight(400))
|
||||
|
||||
// leftWidth = 1000 - 0 (no selected node) - 0 - 400 - 4 = 596
|
||||
// 596 >= 404 so floatingRight should be false
|
||||
expect(result.current.floatingRight).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -80,8 +75,6 @@ describe('useFloatingRight', () => {
|
||||
|
||||
const { result } = renderHook(() => useFloatingRight(400))
|
||||
|
||||
// leftWidth = 1200 - 400 (node panel) - 0 - 400 - 4 = 396
|
||||
// 396 < 404 so floatingRight should be true
|
||||
expect(result.current.floatingRight).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -103,7 +96,6 @@ describe('useFloatingRight', () => {
|
||||
|
||||
const { result } = renderHook(() => useFloatingRight(600))
|
||||
|
||||
// When floating and no selected node, width = min(600, 0 + 200) = 200
|
||||
expect(result.current.floatingRightWidth).toBeLessThanOrEqual(600)
|
||||
})
|
||||
|
||||
@ -115,7 +107,6 @@ describe('useFloatingRight', () => {
|
||||
|
||||
const { result } = renderHook(() => useFloatingRight(600))
|
||||
|
||||
// When floating with selected node, width = min(600, 300 + 100) = 400
|
||||
expect(result.current.floatingRightWidth).toBeLessThanOrEqual(600)
|
||||
})
|
||||
})
|
||||
@ -127,7 +118,6 @@ describe('useFloatingRight', () => {
|
||||
|
||||
const { result } = renderHook(() => useFloatingRight(400))
|
||||
|
||||
// Should not throw and should maintain initial state
|
||||
expect(result.current.floatingRight).toBe(false)
|
||||
})
|
||||
|
||||
@ -145,7 +135,6 @@ describe('useFloatingRight', () => {
|
||||
|
||||
const { result } = renderHook(() => useFloatingRight(10000))
|
||||
|
||||
// Should be floating due to limited space
|
||||
expect(result.current.floatingRight).toBe(true)
|
||||
})
|
||||
|
||||
@ -159,7 +148,6 @@ describe('useFloatingRight', () => {
|
||||
|
||||
const { result } = renderHook(() => useFloatingRight(400))
|
||||
|
||||
// Should have selected node so node panel is considered
|
||||
expect(result.current).toBeDefined()
|
||||
})
|
||||
})
|
||||
@ -5,19 +5,13 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import InputFieldPanel from './index'
|
||||
import InputFieldPanel from '../index'
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Dependencies
|
||||
// ============================================================================
|
||||
|
||||
// Mock reactflow hooks - use getter to allow dynamic updates
|
||||
let mockNodesData: Node<DataSourceNodeType>[] = []
|
||||
vi.mock('reactflow', () => ({
|
||||
useNodes: () => mockNodesData,
|
||||
}))
|
||||
|
||||
// Mock useInputFieldPanel hook
|
||||
const mockCloseAllInputFieldPanels = vi.fn()
|
||||
const mockToggleInputFieldPreviewPanel = vi.fn()
|
||||
let mockIsPreviewing = false
|
||||
@ -32,7 +26,6 @@ vi.mock('@/app/components/rag-pipeline/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useStore (workflow store)
|
||||
let mockRagPipelineVariables: RAGPipelineVariables = []
|
||||
const mockSetRagPipelineVariables = vi.fn()
|
||||
|
||||
@ -56,7 +49,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useNodesSyncDraft hook
|
||||
const mockHandleSyncWorkflowDraft = vi.fn()
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
@ -65,8 +57,7 @@ vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock FieldList component
|
||||
vi.mock('./field-list', () => ({
|
||||
vi.mock('../field-list', () => ({
|
||||
default: ({
|
||||
nodeId,
|
||||
LabelRightContent,
|
||||
@ -124,13 +115,11 @@ vi.mock('./field-list', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock FooterTip component
|
||||
vi.mock('./footer-tip', () => ({
|
||||
vi.mock('../footer-tip', () => ({
|
||||
default: () => <div data-testid="footer-tip">Footer Tip</div>,
|
||||
}))
|
||||
|
||||
// Mock Datasource label component
|
||||
vi.mock('./label-right-content/datasource', () => ({
|
||||
vi.mock('../label-right-content/datasource', () => ({
|
||||
default: ({ nodeData }: { nodeData: DataSourceNodeType }) => (
|
||||
<div data-testid={`datasource-label-${nodeData.title}`}>
|
||||
{nodeData.title}
|
||||
@ -138,15 +127,10 @@ vi.mock('./label-right-content/datasource', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock GlobalInputs label component
|
||||
vi.mock('./label-right-content/global-inputs', () => ({
|
||||
vi.mock('../label-right-content/global-inputs', () => ({
|
||||
default: () => <div data-testid="global-inputs-label">Global Inputs</div>,
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
|
||||
const createInputVar = (overrides?: Partial<InputVar>): InputVar => ({
|
||||
type: PipelineInputVarType.textInput,
|
||||
label: 'Test Label',
|
||||
@ -189,10 +173,6 @@ const createDataSourceNode = (
|
||||
} as DataSourceNodeType,
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
const setupMocks = (options?: {
|
||||
nodes?: Node<DataSourceNodeType>[]
|
||||
ragPipelineVariables?: RAGPipelineVariables
|
||||
@ -205,148 +185,110 @@ const setupMocks = (options?: {
|
||||
mockIsEditing = options?.isEditing || false
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// InputFieldPanel Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('InputFieldPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
setupMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render panel without crashing', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByText('datasetPipeline.inputFieldPanel.title'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render panel title correctly', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByText('datasetPipeline.inputFieldPanel.title'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render panel description', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByText('datasetPipeline.inputFieldPanel.description'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render preview button', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByText('datasetPipeline.operations.preview'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close button', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
const closeButton = screen.getByRole('button', { name: '' })
|
||||
expect(closeButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render footer tip component', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('footer-tip')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render unique inputs section title', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByText('datasetPipeline.inputFieldPanel.uniqueInputs.title'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render global inputs field list', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-shared')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('global-inputs-label')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DataSource Node Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('DataSource Node Rendering', () => {
|
||||
it('should render field list for each datasource node', () => {
|
||||
// Arrange
|
||||
const nodes = [
|
||||
createDataSourceNode('node-1', 'DataSource 1'),
|
||||
createDataSourceNode('node-2', 'DataSource 2'),
|
||||
]
|
||||
setupMocks({ nodes })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-node-1')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('field-list-node-2')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render datasource label for each node', () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'My DataSource')]
|
||||
setupMocks({ nodes })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByTestId('datasource-label-My DataSource'),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not render any datasource field lists when no nodes exist', () => {
|
||||
// Arrange
|
||||
setupMocks({ nodes: [] })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.queryByTestId('field-list-node-1')).not.toBeInTheDocument()
|
||||
// Global inputs should still render
|
||||
expect(screen.getByTestId('field-list-shared')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should filter only DataSource type nodes', () => {
|
||||
// Arrange
|
||||
const dataSourceNode = createDataSourceNode('ds-node', 'DataSource Node')
|
||||
// Create a non-datasource node to verify filtering
|
||||
const otherNode = {
|
||||
id: 'other-node',
|
||||
type: 'custom',
|
||||
@ -359,10 +301,8 @@ describe('InputFieldPanel', () => {
|
||||
} as Node<DataSourceNodeType>
|
||||
mockNodesData = [dataSourceNode, otherNode]
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-ds-node')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByTestId('field-list-other-node'),
|
||||
@ -370,12 +310,8 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Input Fields Map Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Input Fields Map', () => {
|
||||
it('should correctly distribute variables to their nodes', () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
const variables = [
|
||||
createRAGPipelineVariable('node-1', { variable: 'var1' }),
|
||||
@ -384,28 +320,22 @@ describe('InputFieldPanel', () => {
|
||||
]
|
||||
setupMocks({ nodes, ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-fields-count-node-1')).toHaveTextContent('2')
|
||||
expect(screen.getByTestId('field-list-fields-count-shared')).toHaveTextContent('1')
|
||||
})
|
||||
|
||||
it('should show zero fields for nodes without variables', () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
setupMocks({ nodes, ragPipelineVariables: [] })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-fields-count-node-1')).toHaveTextContent('0')
|
||||
})
|
||||
|
||||
it('should pass all variable names to field lists', () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
const variables = [
|
||||
createRAGPipelineVariable('node-1', { variable: 'var1' }),
|
||||
@ -413,10 +343,8 @@ describe('InputFieldPanel', () => {
|
||||
]
|
||||
setupMocks({ nodes, ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-all-vars-node-1')).toHaveTextContent(
|
||||
'var1,var2',
|
||||
)
|
||||
@ -426,48 +354,35 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User Interactions Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
// Helper to identify close button by its class
|
||||
const isCloseButton = (btn: HTMLElement) =>
|
||||
btn.classList.contains('size-6')
|
||||
|| btn.className.includes('shrink-0 items-center justify-center p-0.5')
|
||||
|
||||
it('should call closeAllInputFieldPanels when close button is clicked', () => {
|
||||
// Arrange
|
||||
render(<InputFieldPanel />)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
const closeButton = buttons.find(isCloseButton)
|
||||
|
||||
// Act
|
||||
fireEvent.click(closeButton!)
|
||||
|
||||
// Assert
|
||||
expect(mockCloseAllInputFieldPanels).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call toggleInputFieldPreviewPanel when preview button is clicked', () => {
|
||||
// Arrange
|
||||
render(<InputFieldPanel />)
|
||||
const previewButton = screen.getByText('datasetPipeline.operations.preview')
|
||||
|
||||
// Act
|
||||
fireEvent.click(previewButton)
|
||||
|
||||
// Assert
|
||||
expect(mockToggleInputFieldPreviewPanel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should disable preview button when editing', () => {
|
||||
// Arrange
|
||||
setupMocks({ isEditing: true })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen
|
||||
.getByText('datasetPipeline.operations.preview')
|
||||
.closest('button')
|
||||
@ -475,13 +390,10 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
|
||||
it('should not disable preview button when not editing', () => {
|
||||
// Arrange
|
||||
setupMocks({ isEditing: false })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen
|
||||
.getByText('datasetPipeline.operations.preview')
|
||||
.closest('button')
|
||||
@ -489,18 +401,12 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Preview State Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Preview State', () => {
|
||||
it('should apply active styling when previewing', () => {
|
||||
// Arrange
|
||||
setupMocks({ isPreviewing: true })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
const previewButton = screen
|
||||
.getByText('datasetPipeline.operations.preview')
|
||||
.closest('button')
|
||||
@ -509,81 +415,62 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
|
||||
it('should set readonly to true when previewing', () => {
|
||||
// Arrange
|
||||
setupMocks({ isPreviewing: true })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-readonly-shared')).toHaveTextContent(
|
||||
'true',
|
||||
)
|
||||
})
|
||||
|
||||
it('should set readonly to true when editing', () => {
|
||||
// Arrange
|
||||
setupMocks({ isEditing: true })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-readonly-shared')).toHaveTextContent(
|
||||
'true',
|
||||
)
|
||||
})
|
||||
|
||||
it('should set readonly to false when not previewing or editing', () => {
|
||||
// Arrange
|
||||
setupMocks({ isPreviewing: false, isEditing: false })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-readonly-shared')).toHaveTextContent(
|
||||
'false',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Input Fields Change Handler Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Input Fields Change Handler', () => {
|
||||
it('should update rag pipeline variables when input fields change', async () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
setupMocks({ nodes })
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('trigger-change-node-1'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockSetRagPipelineVariables).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should call handleSyncWorkflowDraft when fields change', async () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
setupMocks({ nodes })
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('trigger-change-node-1'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should place datasource node fields before global fields', async () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
const variables = [
|
||||
createRAGPipelineVariable('shared', { variable: 'shared_var' }),
|
||||
@ -591,15 +478,12 @@ describe('InputFieldPanel', () => {
|
||||
setupMocks({ nodes, ragPipelineVariables: variables })
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('trigger-change-node-1'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockSetRagPipelineVariables).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Verify datasource fields come before shared fields
|
||||
const setVarsCall = mockSetRagPipelineVariables.mock.calls[0][0] as RAGPipelineVariables
|
||||
const isNotShared = (v: RAGPipelineVariable) => v.belong_to_node_id !== 'shared'
|
||||
const isShared = (v: RAGPipelineVariable) => v.belong_to_node_id === 'shared'
|
||||
@ -614,7 +498,6 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
|
||||
it('should handle removing all fields from a node', async () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
const variables = [
|
||||
createRAGPipelineVariable('node-1', { variable: 'var1' }),
|
||||
@ -623,24 +506,19 @@ describe('InputFieldPanel', () => {
|
||||
setupMocks({ nodes, ragPipelineVariables: variables })
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('trigger-remove-node-1'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockSetRagPipelineVariables).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
it('should update global input fields correctly', async () => {
|
||||
// Arrange
|
||||
setupMocks()
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('trigger-change-shared'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockSetRagPipelineVariables).toHaveBeenCalled()
|
||||
})
|
||||
@ -652,54 +530,39 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Label Class Name Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Label Class Names', () => {
|
||||
it('should pass correct className to datasource field lists', () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
setupMocks({ nodes })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByTestId('field-list-classname-node-1'),
|
||||
).toHaveTextContent('pt-1 pb-1')
|
||||
})
|
||||
|
||||
it('should pass correct className to global inputs field list', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-classname-shared')).toHaveTextContent(
|
||||
'pt-2 pb-1',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should memoize datasourceNodeDataMap based on nodes', () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
setupMocks({ nodes })
|
||||
const { rerender } = render(<InputFieldPanel />)
|
||||
|
||||
// Act - rerender with same nodes reference
|
||||
rerender(<InputFieldPanel />)
|
||||
|
||||
// Assert - component should not break and should render correctly
|
||||
expect(screen.getByTestId('field-list-node-1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should compute allVariableNames correctly', () => {
|
||||
// Arrange
|
||||
const variables = [
|
||||
createRAGPipelineVariable('node-1', { variable: 'alpha' }),
|
||||
createRAGPipelineVariable('node-1', { variable: 'beta' }),
|
||||
@ -707,21 +570,15 @@ describe('InputFieldPanel', () => {
|
||||
]
|
||||
setupMocks({ ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-all-vars-shared')).toHaveTextContent(
|
||||
'alpha,beta,gamma',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Callback Stability Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Callback Stability', () => {
|
||||
// Helper to find close button - moved outside test to reduce nesting
|
||||
const findCloseButton = (buttons: HTMLElement[]) => {
|
||||
const isCloseButton = (btn: HTMLElement) =>
|
||||
btn.classList.contains('size-6')
|
||||
@ -730,10 +587,8 @@ describe('InputFieldPanel', () => {
|
||||
}
|
||||
|
||||
it('should maintain closePanel callback reference', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
const buttons1 = screen.getAllByRole('button')
|
||||
fireEvent.click(findCloseButton(buttons1)!)
|
||||
const callCount1 = mockCloseAllInputFieldPanels.mock.calls.length
|
||||
@ -742,126 +597,97 @@ describe('InputFieldPanel', () => {
|
||||
const buttons2 = screen.getAllByRole('button')
|
||||
fireEvent.click(findCloseButton(buttons2)!)
|
||||
|
||||
// Assert
|
||||
expect(mockCloseAllInputFieldPanels.mock.calls.length).toBe(callCount1 + 1)
|
||||
})
|
||||
|
||||
it('should maintain togglePreviewPanel callback reference', () => {
|
||||
// Arrange
|
||||
const { rerender } = render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.preview'))
|
||||
const callCount1 = mockToggleInputFieldPreviewPanel.mock.calls.length
|
||||
|
||||
rerender(<InputFieldPanel />)
|
||||
fireEvent.click(screen.getByText('datasetPipeline.operations.preview'))
|
||||
|
||||
// Assert
|
||||
expect(mockToggleInputFieldPreviewPanel.mock.calls.length).toBe(
|
||||
callCount1 + 1,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty ragPipelineVariables', () => {
|
||||
// Arrange
|
||||
setupMocks({ ragPipelineVariables: [] })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-all-vars-shared')).toHaveTextContent(
|
||||
'',
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle undefined ragPipelineVariables', () => {
|
||||
// Arrange - intentionally testing undefined case
|
||||
// @ts-expect-error Testing edge case with undefined value
|
||||
mockRagPipelineVariables = undefined
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-shared')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle null variable names in allVariableNames', () => {
|
||||
// Arrange - intentionally testing edge case with empty variable name
|
||||
const variables = [
|
||||
createRAGPipelineVariable('node-1', { variable: 'valid_var' }),
|
||||
createRAGPipelineVariable('node-1', { variable: '' }),
|
||||
]
|
||||
setupMocks({ ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert - should not crash
|
||||
expect(screen.getByTestId('field-list-shared')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should handle large number of datasource nodes', () => {
|
||||
// Arrange
|
||||
const nodes = Array.from({ length: 10 }, (_, i) =>
|
||||
createDataSourceNode(`node-${i}`, `DataSource ${i}`))
|
||||
setupMocks({ nodes })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
nodes.forEach((_, i) => {
|
||||
expect(screen.getByTestId(`field-list-node-${i}`)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle large number of variables', () => {
|
||||
// Arrange
|
||||
const variables = Array.from({ length: 100 }, (_, i) =>
|
||||
createRAGPipelineVariable('shared', { variable: `var_${i}` }))
|
||||
setupMocks({ ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-fields-count-shared')).toHaveTextContent(
|
||||
'100',
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle special characters in variable names', () => {
|
||||
// Arrange
|
||||
const variables = [
|
||||
createRAGPipelineVariable('shared', { variable: 'var_with_underscore' }),
|
||||
createRAGPipelineVariable('shared', { variable: 'varWithCamelCase' }),
|
||||
]
|
||||
setupMocks({ ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-all-vars-shared')).toHaveTextContent(
|
||||
'var_with_underscore,varWithCamelCase',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Multiple Nodes Interaction Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Multiple Nodes Interaction', () => {
|
||||
it('should handle changes to multiple nodes sequentially', async () => {
|
||||
// Arrange
|
||||
const nodes = [
|
||||
createDataSourceNode('node-1', 'DataSource 1'),
|
||||
createDataSourceNode('node-2', 'DataSource 2'),
|
||||
@ -869,18 +695,15 @@ describe('InputFieldPanel', () => {
|
||||
setupMocks({ nodes })
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Act
|
||||
fireEvent.click(screen.getByTestId('trigger-change-node-1'))
|
||||
fireEvent.click(screen.getByTestId('trigger-change-node-2'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockSetRagPipelineVariables).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
|
||||
it('should maintain separate field lists for different nodes', () => {
|
||||
// Arrange
|
||||
const nodes = [
|
||||
createDataSourceNode('node-1', 'DataSource 1'),
|
||||
createDataSourceNode('node-2', 'DataSource 2'),
|
||||
@ -892,42 +715,31 @@ describe('InputFieldPanel', () => {
|
||||
]
|
||||
setupMocks({ nodes, ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-fields-count-node-1')).toHaveTextContent('1')
|
||||
expect(screen.getByTestId('field-list-fields-count-node-2')).toHaveTextContent('2')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Component Structure Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Component Structure', () => {
|
||||
it('should have correct panel width class', () => {
|
||||
// Act
|
||||
const { container } = render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
const panel = container.firstChild as HTMLElement
|
||||
expect(panel).toHaveClass('w-[400px]')
|
||||
})
|
||||
|
||||
it('should have overflow scroll on content area', () => {
|
||||
// Act
|
||||
const { container } = render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
const scrollContainer = container.querySelector('.overflow-y-auto')
|
||||
expect(scrollContainer).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render header section with proper spacing', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByText('datasetPipeline.inputFieldPanel.title'),
|
||||
).toBeInTheDocument()
|
||||
@ -937,12 +749,8 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Integration with FieldList Component Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Integration with FieldList Component', () => {
|
||||
it('should pass correct props to FieldList for datasource nodes', () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
const variables = [
|
||||
createRAGPipelineVariable('node-1', { variable: 'test_var' }),
|
||||
@ -953,38 +761,29 @@ describe('InputFieldPanel', () => {
|
||||
isPreviewing: true,
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-node-1')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('field-list-readonly-node-1')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('field-list-fields-count-node-1')).toHaveTextContent('1')
|
||||
})
|
||||
|
||||
it('should pass correct props to FieldList for shared node', () => {
|
||||
// Arrange
|
||||
const variables = [
|
||||
createRAGPipelineVariable('shared', { variable: 'shared_var' }),
|
||||
]
|
||||
setupMocks({ ragPipelineVariables: variables, isEditing: true })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-shared')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('field-list-readonly-shared')).toHaveTextContent('true')
|
||||
expect(screen.getByTestId('field-list-fields-count-shared')).toHaveTextContent('1')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Variable Ordering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Variable Ordering', () => {
|
||||
it('should maintain correct variable order in allVariableNames', () => {
|
||||
// Arrange
|
||||
const variables = [
|
||||
createRAGPipelineVariable('node-1', { variable: 'first' }),
|
||||
createRAGPipelineVariable('node-1', { variable: 'second' }),
|
||||
@ -992,10 +791,8 @@ describe('InputFieldPanel', () => {
|
||||
]
|
||||
setupMocks({ ragPipelineVariables: variables })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('field-list-all-vars-shared')).toHaveTextContent(
|
||||
'first,second,third',
|
||||
)
|
||||
@ -1003,13 +800,8 @@ describe('InputFieldPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// useFloatingRight Hook Integration Tests (via InputFieldPanel)
|
||||
// ============================================================================
|
||||
|
||||
describe('useFloatingRight Hook Integration', () => {
|
||||
// Note: The hook is tested indirectly through the InputFieldPanel component
|
||||
// as it's used internally. Direct hook tests are in hooks.spec.tsx if exists.
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1017,16 +809,11 @@ describe('useFloatingRight Hook Integration', () => {
|
||||
})
|
||||
|
||||
it('should render panel correctly with default floating state', () => {
|
||||
// The hook is mocked via the component's behavior
|
||||
render(<InputFieldPanel />)
|
||||
expect(screen.getByTestId('field-list-shared')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// FooterTip Component Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('FooterTip Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1034,18 +821,12 @@ describe('FooterTip Integration', () => {
|
||||
})
|
||||
|
||||
it('should render footer tip at the bottom of the panel', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('footer-tip')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Label Components Integration Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Label Components Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1053,25 +834,20 @@ describe('Label Components Integration', () => {
|
||||
})
|
||||
|
||||
it('should render GlobalInputs label for shared field list', () => {
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByTestId('global-inputs-label')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render Datasource label for each datasource node', () => {
|
||||
// Arrange
|
||||
const nodes = [
|
||||
createDataSourceNode('node-1', 'First DataSource'),
|
||||
createDataSourceNode('node-2', 'Second DataSource'),
|
||||
]
|
||||
setupMocks({ nodes })
|
||||
|
||||
// Act
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Assert
|
||||
expect(
|
||||
screen.getByTestId('datasource-label-First DataSource'),
|
||||
).toBeInTheDocument()
|
||||
@ -1081,10 +857,6 @@ describe('Label Components Integration', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Component Memo Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Component Memo Behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1092,14 +864,10 @@ describe('Component Memo Behavior', () => {
|
||||
})
|
||||
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// InputFieldPanel is exported as memo(InputFieldPanel)
|
||||
// This test ensures the component doesn't break memoization
|
||||
const { rerender } = render(<InputFieldPanel />)
|
||||
|
||||
// Act - rerender without prop changes
|
||||
rerender(<InputFieldPanel />)
|
||||
|
||||
// Assert - component should still render correctly
|
||||
expect(screen.getByTestId('field-list-shared')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('datasetPipeline.inputFieldPanel.title'),
|
||||
@ -1107,15 +875,12 @@ describe('Component Memo Behavior', () => {
|
||||
})
|
||||
|
||||
it('should handle state updates correctly with memo', async () => {
|
||||
// Arrange
|
||||
const nodes = [createDataSourceNode('node-1', 'DataSource 1')]
|
||||
setupMocks({ nodes })
|
||||
render(<InputFieldPanel />)
|
||||
|
||||
// Act - trigger a state change
|
||||
fireEvent.click(screen.getByTestId('trigger-change-node-1'))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => {
|
||||
expect(mockSetRagPipelineVariables).toHaveBeenCalled()
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import { useConfigurations, useHiddenConfigurations, useHiddenFieldNames } from './hooks'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { useConfigurations, useHiddenConfigurations, useHiddenFieldNames } from '../hooks'
|
||||
|
||||
vi.mock('@/app/components/base/file-uploader/hooks', () => ({
|
||||
useFileSizeLimit: () => ({
|
||||
@ -35,7 +25,7 @@ vi.mock('@/app/components/workflow/constants', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('./schema', () => ({
|
||||
vi.mock('../schema', () => ({
|
||||
TEXT_MAX_LENGTH: 256,
|
||||
}))
|
||||
|
||||
@ -43,10 +33,6 @@ vi.mock('@/utils/format', () => ({
|
||||
formatFileSize: (size: number) => `${Math.round(size / 1024 / 1024)}MB`,
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useHiddenFieldNames', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -71,53 +57,51 @@ describe('useHiddenFieldNames', () => {
|
||||
it('should return field names for number type including unit', () => {
|
||||
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.number))
|
||||
|
||||
expect(result.current).toContain('variableconfig.defaultvalue')
|
||||
expect(result.current).toContain('variableconfig.unit')
|
||||
expect(result.current).toContain('variableconfig.placeholder')
|
||||
expect(result.current).toContain('variableconfig.tooltips')
|
||||
expect(result.current).toContain('appdebug.variableconfig.defaultvalue')
|
||||
expect(result.current).toContain('appdebug.variableconfig.unit')
|
||||
expect(result.current).toContain('appdebug.variableconfig.placeholder')
|
||||
expect(result.current).toContain('appdebug.variableconfig.tooltips')
|
||||
})
|
||||
|
||||
it('should return field names for select type', () => {
|
||||
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.select))
|
||||
|
||||
expect(result.current).toContain('variableconfig.defaultvalue')
|
||||
expect(result.current).toContain('variableconfig.tooltips')
|
||||
expect(result.current).toContain('appdebug.variableconfig.defaultvalue')
|
||||
expect(result.current).toContain('appdebug.variableconfig.tooltips')
|
||||
})
|
||||
|
||||
it('should return field names for singleFile type', () => {
|
||||
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.singleFile))
|
||||
|
||||
expect(result.current).toContain('variableconfig.uploadmethod')
|
||||
expect(result.current).toContain('variableconfig.tooltips')
|
||||
expect(result.current).toContain('appdebug.variableconfig.uploadmethod')
|
||||
expect(result.current).toContain('appdebug.variableconfig.tooltips')
|
||||
})
|
||||
|
||||
it('should return field names for multiFiles type including max number', () => {
|
||||
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.multiFiles))
|
||||
|
||||
expect(result.current).toContain('variableconfig.uploadmethod')
|
||||
expect(result.current).toContain('variableconfig.maxnumberofuploads')
|
||||
expect(result.current).toContain('variableconfig.tooltips')
|
||||
expect(result.current).toContain('appdebug.variableconfig.uploadmethod')
|
||||
expect(result.current).toContain('appdebug.variableconfig.maxnumberofuploads')
|
||||
expect(result.current).toContain('appdebug.variableconfig.tooltips')
|
||||
})
|
||||
|
||||
it('should return field names for checkbox type', () => {
|
||||
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.checkbox))
|
||||
|
||||
expect(result.current).toContain('variableconfig.startchecked')
|
||||
expect(result.current).toContain('variableconfig.tooltips')
|
||||
expect(result.current).toContain('appdebug.variableconfig.startchecked')
|
||||
expect(result.current).toContain('appdebug.variableconfig.tooltips')
|
||||
})
|
||||
|
||||
it('should return only tooltips for unknown type', () => {
|
||||
const { result } = renderHook(() => useHiddenFieldNames('unknown-type' as PipelineInputVarType))
|
||||
|
||||
expect(result.current).toBe('variableconfig.tooltips')
|
||||
expect(result.current).toBe('appdebug.variableconfig.tooltips')
|
||||
})
|
||||
|
||||
it('should return comma-separated lowercase string', () => {
|
||||
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.textInput))
|
||||
|
||||
// The result should be comma-separated
|
||||
expect(result.current).toMatch(/,/)
|
||||
// All lowercase
|
||||
expect(result.current).toBe(result.current.toLowerCase())
|
||||
})
|
||||
})
|
||||
@ -343,7 +327,6 @@ describe('useHiddenConfigurations', () => {
|
||||
c => c.variable === 'default' && c.showConditions?.some(sc => sc.value === PipelineInputVarType.select),
|
||||
)
|
||||
expect(selectDefault?.options).toBeDefined()
|
||||
// First option should be "no default selected"
|
||||
expect(selectDefault?.options?.[0]?.value).toBe('')
|
||||
expect(selectDefault?.options?.[1]?.value).toBe('opt1')
|
||||
expect(selectDefault?.options?.[2]?.value).toBe('opt2')
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,25 +1,16 @@
|
||||
import type { TFunction } from 'i18next'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
import { createInputFieldSchema, TEXT_MAX_LENGTH } from './schema'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
import { createInputFieldSchema, TEXT_MAX_LENGTH } from '../schema'
|
||||
|
||||
vi.mock('@/config', () => ({
|
||||
MAX_VAR_KEY_LENGTH: 30,
|
||||
}))
|
||||
|
||||
// Minimal t function for testing
|
||||
const t: TFunction = ((key: string) => key) as unknown as TFunction
|
||||
|
||||
const defaultOptions = { maxFileUploadLimit: 10 }
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('TEXT_MAX_LENGTH', () => {
|
||||
it('should be 256', () => {
|
||||
expect(TEXT_MAX_LENGTH).toBe(256)
|
||||
@ -2,17 +2,7 @@ import type { InputVar } from '@/models/pipeline'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { act } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useFieldList } from './hooks'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { useFieldList } from '../hooks'
|
||||
|
||||
const mockToggleInputFieldEditPanel = vi.fn()
|
||||
vi.mock('@/app/components/rag-pipeline/hooks', () => ({
|
||||
@ -24,7 +14,7 @@ vi.mock('@/app/components/rag-pipeline/hooks', () => ({
|
||||
const mockHandleInputVarRename = vi.fn()
|
||||
const mockIsVarUsedInNodes = vi.fn()
|
||||
const mockRemoveUsedVarInNodes = vi.fn()
|
||||
vi.mock('../../../../hooks/use-pipeline', () => ({
|
||||
vi.mock('../../../../../hooks/use-pipeline', () => ({
|
||||
usePipeline: () => ({
|
||||
handleInputVarRename: mockHandleInputVarRename,
|
||||
isVarUsedInNodes: mockIsVarUsedInNodes,
|
||||
@ -46,10 +36,6 @@ vi.mock('@/app/components/workflow/types', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test data helpers
|
||||
// ============================================================================
|
||||
|
||||
function createInputVar(overrides?: Partial<InputVar>): InputVar {
|
||||
return {
|
||||
type: 'text-input',
|
||||
@ -70,10 +56,6 @@ function createDefaultProps(overrides?: Partial<Parameters<typeof useFieldList>[
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useFieldList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -216,14 +198,12 @@ describe('useFieldList', () => {
|
||||
})),
|
||||
)
|
||||
|
||||
// Trigger remove to set up confirmation state
|
||||
act(() => {
|
||||
result.current.handleRemoveField(0)
|
||||
})
|
||||
|
||||
expect(result.current.isShowRemoveVarConfirm).toBe(true)
|
||||
|
||||
// Confirm removal
|
||||
act(() => {
|
||||
result.current.onRemoveVarConfirm()
|
||||
})
|
||||
@ -292,12 +272,10 @@ describe('useFieldList', () => {
|
||||
useFieldList(createDefaultProps({ onInputFieldsChange })),
|
||||
)
|
||||
|
||||
// Open editor for new field
|
||||
act(() => {
|
||||
result.current.handleOpenInputFieldEditor()
|
||||
})
|
||||
|
||||
// Get the onSubmit callback from editor
|
||||
const editorProps = mockToggleInputFieldEditPanel.mock.calls[0][0]
|
||||
const newField = createInputVar({ variable: 'new_var', label: 'New' })
|
||||
|
||||
@ -319,7 +297,6 @@ describe('useFieldList', () => {
|
||||
})),
|
||||
)
|
||||
|
||||
// Open editor for new field (not editing existing)
|
||||
act(() => {
|
||||
result.current.handleOpenInputFieldEditor()
|
||||
})
|
||||
@ -349,7 +326,6 @@ describe('useFieldList', () => {
|
||||
})),
|
||||
)
|
||||
|
||||
// Open editor for existing field
|
||||
act(() => {
|
||||
result.current.handleOpenInputFieldEditor('old_name')
|
||||
})
|
||||
@ -381,13 +357,11 @@ describe('useFieldList', () => {
|
||||
useFieldList(createDefaultProps({ initialInputFields: [var1] })),
|
||||
)
|
||||
|
||||
// Show confirmation
|
||||
act(() => {
|
||||
result.current.handleRemoveField(0)
|
||||
})
|
||||
expect(result.current.isShowRemoveVarConfirm).toBe(true)
|
||||
|
||||
// Hide confirmation
|
||||
act(() => {
|
||||
result.current.hideRemoveVarConfirm()
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,17 +2,9 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-so
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import Datasource from './datasource'
|
||||
import GlobalInputs from './global-inputs'
|
||||
import Datasource from '../datasource'
|
||||
import GlobalInputs from '../global-inputs'
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock BlockIcon
|
||||
vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
default: ({ type, toolIcon, className }: { type: BlockEnum, toolIcon?: string, className?: string }) => (
|
||||
<div
|
||||
@ -24,12 +16,10 @@ vi.mock('@/app/components/workflow/block-icon', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock useToolIcon
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useToolIcon: (nodeData: DataSourceNodeType) => nodeData.provider_name || 'default-icon',
|
||||
}))
|
||||
|
||||
// Mock Tooltip
|
||||
vi.mock('@/app/components/base/tooltip', () => ({
|
||||
default: ({ popupContent, popupClassName }: { popupContent: string, popupClassName?: string }) => (
|
||||
<div data-testid="tooltip" data-content={popupContent} className={popupClassName} />
|
||||
@ -132,7 +122,6 @@ describe('Datasource', () => {
|
||||
|
||||
render(<Datasource nodeData={nodeData} />)
|
||||
|
||||
// Should still render without the title text
|
||||
expect(screen.getByTestId('block-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -160,13 +149,13 @@ describe('GlobalInputs', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<GlobalInputs />)
|
||||
|
||||
expect(screen.getByText('inputFieldPanel.globalInputs.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.inputFieldPanel.globalInputs.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render title with correct translation key', () => {
|
||||
render(<GlobalInputs />)
|
||||
|
||||
expect(screen.getByText('inputFieldPanel.globalInputs.title')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetPipeline.inputFieldPanel.globalInputs.title')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render tooltip component', () => {
|
||||
@ -179,7 +168,7 @@ describe('GlobalInputs', () => {
|
||||
render(<GlobalInputs />)
|
||||
|
||||
const tooltip = screen.getByTestId('tooltip')
|
||||
expect(tooltip).toHaveAttribute('data-content', 'inputFieldPanel.globalInputs.tooltip')
|
||||
expect(tooltip).toHaveAttribute('data-content', 'datasetPipeline.inputFieldPanel.globalInputs.tooltip')
|
||||
})
|
||||
|
||||
it('should have correct tooltip className', () => {
|
||||
@ -199,7 +188,7 @@ describe('GlobalInputs', () => {
|
||||
it('should have correct title styling', () => {
|
||||
render(<GlobalInputs />)
|
||||
|
||||
const titleElement = screen.getByText('inputFieldPanel.globalInputs.title')
|
||||
const titleElement = screen.getByText('datasetPipeline.inputFieldPanel.globalInputs.title')
|
||||
expect(titleElement).toHaveClass('system-sm-semibold-uppercase', 'text-text-secondary')
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -3,15 +3,9 @@ import type { WorkflowRunningData } from '@/app/components/workflow/types'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import Header from './header'
|
||||
// Import components after mocks
|
||||
import TestRunPanel from './index'
|
||||
import Header from '../header'
|
||||
import TestRunPanel from '../index'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock workflow store
|
||||
const mockIsPreparingDataSource = vi.fn(() => true)
|
||||
const mockSetIsPreparingDataSource = vi.fn()
|
||||
const mockWorkflowRunningData = vi.fn<() => WorkflowRunningData | undefined>(() => undefined)
|
||||
@ -34,7 +28,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow interactions
|
||||
const mockHandleCancelDebugAndPreviewPanel = vi.fn()
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowInteractions: () => ({
|
||||
@ -46,22 +39,18 @@ vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useToolIcon: () => 'mock-tool-icon',
|
||||
}))
|
||||
|
||||
// Mock data source provider
|
||||
vi.mock('@/app/components/datasets/documents/create-from-pipeline/data-source/store/provider', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <div data-testid="data-source-provider">{children}</div>,
|
||||
}))
|
||||
|
||||
// Mock Preparation component
|
||||
vi.mock('./preparation', () => ({
|
||||
vi.mock('../preparation', () => ({
|
||||
default: () => <div data-testid="preparation-component">Preparation</div>,
|
||||
}))
|
||||
|
||||
// Mock Result component (for TestRunPanel tests only)
|
||||
vi.mock('./result', () => ({
|
||||
vi.mock('../result', () => ({
|
||||
default: () => <div data-testid="result-component">Result</div>,
|
||||
}))
|
||||
|
||||
// Mock ResultPanel from workflow
|
||||
vi.mock('@/app/components/workflow/run/result-panel', () => ({
|
||||
default: (props: Record<string, unknown>) => (
|
||||
<div data-testid="result-panel">
|
||||
@ -72,7 +61,6 @@ vi.mock('@/app/components/workflow/run/result-panel', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock TracingPanel from workflow
|
||||
vi.mock('@/app/components/workflow/run/tracing-panel', () => ({
|
||||
default: (props: { list: unknown[] }) => (
|
||||
<div data-testid="tracing-panel">
|
||||
@ -85,20 +73,14 @@ vi.mock('@/app/components/workflow/run/tracing-panel', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock Loading component
|
||||
vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div data-testid="loading">Loading...</div>,
|
||||
}))
|
||||
|
||||
// Mock config
|
||||
vi.mock('@/config', () => ({
|
||||
RAG_PIPELINE_PREVIEW_CHUNK_NUM: 5,
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
|
||||
const createMockWorkflowRunningData = (overrides: Partial<WorkflowRunningData> = {}): WorkflowRunningData => ({
|
||||
result: {
|
||||
status: WorkflowRunningStatus.Succeeded,
|
||||
@ -141,10 +123,6 @@ const createMockQAOutputs = () => ({
|
||||
],
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// TestRunPanel Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('TestRunPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -152,7 +130,6 @@ describe('TestRunPanel', () => {
|
||||
mockWorkflowRunningData.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
// Basic rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render with correct container styles', () => {
|
||||
const { container } = render(<TestRunPanel />)
|
||||
@ -168,7 +145,6 @@ describe('TestRunPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Conditional rendering based on isPreparingDataSource
|
||||
describe('Conditional Content Rendering', () => {
|
||||
it('should render Preparation inside DataSourceProvider when isPreparingDataSource is true', () => {
|
||||
mockIsPreparingDataSource.mockReturnValue(true)
|
||||
@ -192,17 +168,12 @@ describe('TestRunPanel', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Header Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Header', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsPreparingDataSource.mockReturnValue(true)
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render title with correct translation key', () => {
|
||||
render(<Header />)
|
||||
@ -225,7 +196,6 @@ describe('Header', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Close button interactions
|
||||
describe('Close Button Interaction', () => {
|
||||
it('should call setIsPreparingDataSource(false) and handleCancelDebugAndPreviewPanel when clicked and isPreparingDataSource is true', () => {
|
||||
mockIsPreparingDataSource.mockReturnValue(true)
|
||||
@ -253,19 +223,13 @@ describe('Header', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Result Component Tests (Real Implementation)
|
||||
// ============================================================================
|
||||
|
||||
// Unmock Result for these tests
|
||||
vi.doUnmock('./result')
|
||||
vi.doUnmock('../result')
|
||||
|
||||
describe('Result', () => {
|
||||
// Dynamically import Result to get real implementation
|
||||
let Result: typeof import('./result').default
|
||||
let Result: typeof import('../result').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const resultModule = await import('./result')
|
||||
const resultModule = await import('../result')
|
||||
Result = resultModule.default
|
||||
})
|
||||
|
||||
@ -274,7 +238,6 @@ describe('Result', () => {
|
||||
mockWorkflowRunningData.mockReturnValue(undefined)
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render with RESULT tab active by default', async () => {
|
||||
render(<Result />)
|
||||
@ -294,7 +257,6 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Tab switching tests
|
||||
describe('Tab Switching', () => {
|
||||
it('should switch to DETAIL tab when clicked', async () => {
|
||||
mockWorkflowRunningData.mockReturnValue(createMockWorkflowRunningData())
|
||||
@ -321,7 +283,6 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Loading states
|
||||
describe('Loading States', () => {
|
||||
it('should show loading in DETAIL tab when no result data', async () => {
|
||||
mockWorkflowRunningData.mockReturnValue({
|
||||
@ -352,18 +313,13 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// ResultPreview Component Tests
|
||||
// ============================================================================
|
||||
|
||||
// We need to import ResultPreview directly
|
||||
vi.doUnmock('./result/result-preview')
|
||||
vi.doUnmock('../result/result-preview')
|
||||
|
||||
describe('ResultPreview', () => {
|
||||
let ResultPreview: typeof import('./result/result-preview').default
|
||||
let ResultPreview: typeof import('../result/result-preview').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const previewModule = await import('./result/result-preview')
|
||||
const previewModule = await import('../result/result-preview')
|
||||
ResultPreview = previewModule.default
|
||||
})
|
||||
|
||||
@ -373,7 +329,6 @@ describe('ResultPreview', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Loading state
|
||||
describe('Loading State', () => {
|
||||
it('should show loading spinner when isRunning is true and no outputs', () => {
|
||||
render(
|
||||
@ -402,7 +357,6 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Error state
|
||||
describe('Error State', () => {
|
||||
it('should show error message when not running and has error', () => {
|
||||
render(
|
||||
@ -448,7 +402,6 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Success state with outputs
|
||||
describe('Success State with Outputs', () => {
|
||||
it('should render chunk content when outputs are available', () => {
|
||||
render(
|
||||
@ -460,7 +413,6 @@ describe('ResultPreview', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Check that chunk content is rendered (the real ChunkCardList renders the content)
|
||||
expect(screen.getByText('test chunk content')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -492,7 +444,6 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle empty outputs gracefully', () => {
|
||||
render(
|
||||
@ -504,7 +455,6 @@ describe('ResultPreview', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should not crash and should not show chunk card list
|
||||
expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -523,17 +473,13 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tabs Component Tests
|
||||
// ============================================================================
|
||||
|
||||
vi.doUnmock('./result/tabs')
|
||||
vi.doUnmock('../result/tabs')
|
||||
|
||||
describe('Tabs', () => {
|
||||
let Tabs: typeof import('./result/tabs').default
|
||||
let Tabs: typeof import('../result/tabs').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const tabsModule = await import('./result/tabs')
|
||||
const tabsModule = await import('../result/tabs')
|
||||
Tabs = tabsModule.default
|
||||
})
|
||||
|
||||
@ -543,7 +489,6 @@ describe('Tabs', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render all three tabs', () => {
|
||||
render(
|
||||
@ -560,7 +505,6 @@ describe('Tabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Active tab styling
|
||||
describe('Active Tab Styling', () => {
|
||||
it('should highlight RESULT tab when currentTab is RESULT', () => {
|
||||
render(
|
||||
@ -589,7 +533,6 @@ describe('Tabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Tab click handling
|
||||
describe('Tab Click Handling', () => {
|
||||
it('should call switchTab with RESULT when RESULT tab is clicked', () => {
|
||||
render(
|
||||
@ -634,7 +577,6 @@ describe('Tabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Disabled state when no data
|
||||
describe('Disabled State', () => {
|
||||
it('should disable tabs when workflowRunningData is undefined', () => {
|
||||
render(
|
||||
@ -651,17 +593,13 @@ describe('Tabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tab Component Tests
|
||||
// ============================================================================
|
||||
|
||||
vi.doUnmock('./result/tabs/tab')
|
||||
vi.doUnmock('../result/tabs/tab')
|
||||
|
||||
describe('Tab', () => {
|
||||
let Tab: typeof import('./result/tabs/tab').default
|
||||
let Tab: typeof import('../result/tabs/tab').default
|
||||
|
||||
beforeAll(async () => {
|
||||
const tabModule = await import('./result/tabs/tab')
|
||||
const tabModule = await import('../result/tabs/tab')
|
||||
Tab = tabModule.default
|
||||
})
|
||||
|
||||
@ -671,7 +609,6 @@ describe('Tab', () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// Rendering tests
|
||||
describe('Rendering', () => {
|
||||
it('should render tab with label', () => {
|
||||
render(
|
||||
@ -688,7 +625,6 @@ describe('Tab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Active state styling
|
||||
describe('Active State', () => {
|
||||
it('should have active styles when isActive is true', () => {
|
||||
render(
|
||||
@ -721,7 +657,6 @@ describe('Tab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Click handling
|
||||
describe('Click Handling', () => {
|
||||
it('should call onClick with value when clicked', () => {
|
||||
render(
|
||||
@ -753,12 +688,10 @@ describe('Tab', () => {
|
||||
const tab = screen.getByRole('button')
|
||||
fireEvent.click(tab)
|
||||
|
||||
// The click handler is still called, but button is disabled
|
||||
expect(tab).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// Disabled state
|
||||
describe('Disabled State', () => {
|
||||
it('should be disabled when workflowRunningData is undefined', () => {
|
||||
render(
|
||||
@ -793,19 +726,14 @@ describe('Tab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// formatPreviewChunks Utility Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('formatPreviewChunks', () => {
|
||||
let formatPreviewChunks: typeof import('./result/result-preview/utils').formatPreviewChunks
|
||||
let formatPreviewChunks: typeof import('../result/result-preview/utils').formatPreviewChunks
|
||||
|
||||
beforeAll(async () => {
|
||||
const utilsModule = await import('./result/result-preview/utils')
|
||||
const utilsModule = await import('../result/result-preview/utils')
|
||||
formatPreviewChunks = utilsModule.formatPreviewChunks
|
||||
})
|
||||
|
||||
// Edge cases
|
||||
describe('Edge Cases', () => {
|
||||
it('should return undefined for null outputs', () => {
|
||||
expect(formatPreviewChunks(null)).toBeUndefined()
|
||||
@ -824,7 +752,6 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// General (text) chunks
|
||||
describe('General Chunks (ChunkingMode.text)', () => {
|
||||
it('should format general chunks correctly', () => {
|
||||
const outputs = createMockGeneralOutputs(['content1', 'content2', 'content3'])
|
||||
@ -842,7 +769,6 @@ describe('formatPreviewChunks', () => {
|
||||
const outputs = createMockGeneralOutputs(manyChunks)
|
||||
const result = formatPreviewChunks(outputs) as GeneralChunks
|
||||
|
||||
// RAG_PIPELINE_PREVIEW_CHUNK_NUM is mocked to 5
|
||||
expect(result).toHaveLength(5)
|
||||
expect(result).toEqual([
|
||||
{ content: 'chunk0', summary: undefined },
|
||||
@ -861,7 +787,6 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Parent-child chunks
|
||||
describe('Parent-Child Chunks (ChunkingMode.parentChild)', () => {
|
||||
it('should format paragraph mode parent-child chunks correctly', () => {
|
||||
const outputs = createMockParentChildOutputs('paragraph')
|
||||
@ -902,7 +827,6 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// QA chunks
|
||||
describe('QA Chunks (ChunkingMode.qa)', () => {
|
||||
it('should format QA chunks correctly', () => {
|
||||
const outputs = createMockQAOutputs()
|
||||
@ -931,14 +855,10 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Types Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Types', () => {
|
||||
describe('TestRunStep Enum', () => {
|
||||
it('should have correct enum values', async () => {
|
||||
const { TestRunStep } = await import('./types')
|
||||
const { TestRunStep } = await import('../types')
|
||||
|
||||
expect(TestRunStep.dataSource).toBe('dataSource')
|
||||
expect(TestRunStep.documentProcessing).toBe('documentProcessing')
|
||||
@ -3,17 +3,7 @@ import { renderHook } from '@testing-library/react'
|
||||
import { act } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useDatasourceOptions, useOnlineDocument, useOnlineDrive, useTestRunSteps, useWebsiteCrawl } from './hooks'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { useDatasourceOptions, useOnlineDocument, useOnlineDrive, useTestRunSteps, useWebsiteCrawl } from '../hooks'
|
||||
|
||||
const mockNodes: Array<{ id: string, data: Partial<DataSourceNodeType> & { type: string } }> = []
|
||||
vi.mock('reactflow', () => ({
|
||||
@ -33,7 +23,7 @@ vi.mock('@/app/components/workflow/types', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../types', () => ({
|
||||
vi.mock('../../types', () => ({
|
||||
TestRunStep: {
|
||||
dataSource: 'dataSource',
|
||||
documentProcessing: 'documentProcessing',
|
||||
@ -46,10 +36,6 @@ vi.mock('@/models/datasets', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useTestRunSteps', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -82,13 +68,11 @@ describe('useTestRunSteps', () => {
|
||||
it('should decrement step on handleBackStep', () => {
|
||||
const { result } = renderHook(() => useTestRunSteps())
|
||||
|
||||
// Go to step 2 first
|
||||
act(() => {
|
||||
result.current.handleNextStep()
|
||||
})
|
||||
expect(result.current.currentStep).toBe(2)
|
||||
|
||||
// Go back
|
||||
act(() => {
|
||||
result.current.handleBackStep()
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,49 +1,33 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import Actions from './index'
|
||||
|
||||
// ============================================================================
|
||||
// Actions Component Tests
|
||||
// ============================================================================
|
||||
import Actions from '../index'
|
||||
|
||||
describe('Actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render button with translated text', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Translation mock returns key with namespace prefix
|
||||
expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render with correct container structure', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { container } = render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
const wrapper = container.firstChild as HTMLElement
|
||||
expect(wrapper.className).toContain('flex')
|
||||
expect(wrapper.className).toContain('justify-end')
|
||||
@ -52,197 +36,143 @@ describe('Actions', () => {
|
||||
})
|
||||
|
||||
it('should render span with px-0.5 class around text', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { container } = render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
const span = container.querySelector('span')
|
||||
expect(span).toBeInTheDocument()
|
||||
expect(span?.className).toContain('px-0.5')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Props Variations Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Props Variations', () => {
|
||||
it('should pass disabled=true to button when disabled prop is true', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should pass disabled=false to button when disabled prop is false', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should not disable button when disabled prop is undefined', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle disabled switching from true to false', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={true} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
|
||||
// Act - Rerender with disabled=false
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Now enabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle disabled switching from false to true', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially enabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Act - Rerender with disabled=true
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Now disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle undefined disabled becoming true', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially not disabled (undefined)
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Act - Rerender with disabled=true
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Now disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User Interaction Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should call handleNextStep when button is clicked', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call handleNextStep exactly once per click', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalled()
|
||||
expect(handleNextStep.mock.calls).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should call handleNextStep multiple times on multiple clicks', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
const button = screen.getByRole('button')
|
||||
fireEvent.click(button)
|
||||
fireEvent.click(button)
|
||||
fireEvent.click(button)
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(3)
|
||||
})
|
||||
|
||||
it('should not call handleNextStep when button is disabled and clicked', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - Disabled button should not trigger onClick
|
||||
expect(handleNextStep).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle rapid clicks when not disabled', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
const button = screen.getByRole('button')
|
||||
|
||||
// Simulate rapid clicks
|
||||
for (let i = 0; i < 10; i++)
|
||||
fireEvent.click(button)
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(10)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Callback Stability Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Callback Stability', () => {
|
||||
it('should use the new handleNextStep when prop changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep1 = vi.fn()
|
||||
const handleNextStep2 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep1} />,
|
||||
)
|
||||
@ -251,16 +181,13 @@ describe('Actions', () => {
|
||||
rerender(<Actions handleNextStep={handleNextStep2} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should maintain functionality after rerender with same props', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep} />,
|
||||
)
|
||||
@ -269,17 +196,14 @@ describe('Actions', () => {
|
||||
rerender(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should work correctly when handleNextStep changes multiple times', () => {
|
||||
// Arrange
|
||||
const handleNextStep1 = vi.fn()
|
||||
const handleNextStep2 = vi.fn()
|
||||
const handleNextStep3 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep1} />,
|
||||
)
|
||||
@ -291,77 +215,58 @@ describe('Actions', () => {
|
||||
rerender(<Actions handleNextStep={handleNextStep3} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep2).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep3).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped with React.memo', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act - Verify component is memoized by checking display name pattern
|
||||
const { rerender } = render(
|
||||
<Actions handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Rerender with same props should work without issues
|
||||
rerender(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Component should render correctly after rerender
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not break when props remain the same across rerenders', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Multiple rerenders with same props
|
||||
for (let i = 0; i < 5; i++) {
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
}
|
||||
|
||||
// Assert - Should still function correctly
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should update correctly when only disabled prop changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Initially not disabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Act - Change only disabled prop
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Should reflect the new disabled state
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should update correctly when only handleNextStep prop changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep1 = vi.fn()
|
||||
const handleNextStep2 = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep1} />,
|
||||
)
|
||||
@ -369,169 +274,124 @@ describe('Actions', () => {
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Act - Change only handleNextStep prop
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep2} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert - New callback should be used
|
||||
expect(handleNextStep1).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should call handleNextStep even if it has side effects', () => {
|
||||
// Arrange
|
||||
let sideEffectValue = 0
|
||||
const handleNextStep = vi.fn(() => {
|
||||
sideEffectValue = 42
|
||||
})
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
expect(sideEffectValue).toBe(42)
|
||||
})
|
||||
|
||||
it('should handle handleNextStep that returns a value', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn(() => 'return value')
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
expect(handleNextStep).toHaveReturnedWith('return value')
|
||||
})
|
||||
|
||||
it('should handle handleNextStep that is async', async () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
|
||||
// Assert
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should render correctly with both disabled=true and handleNextStep', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should handle component unmount gracefully', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { unmount } = render(<Actions handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Unmount should not throw
|
||||
expect(() => unmount()).not.toThrow()
|
||||
})
|
||||
|
||||
it('should handle disabled as boolean-like falsy value', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act - Test with explicit false
|
||||
render(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Accessibility Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Accessibility', () => {
|
||||
it('should have button element that can receive focus', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions handleNextStep={handleNextStep} />)
|
||||
const button = screen.getByRole('button')
|
||||
|
||||
// Assert - Button should be focusable (not disabled by default)
|
||||
expect(button).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('should indicate disabled state correctly', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
render(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert
|
||||
expect(screen.getByRole('button')).toHaveAttribute('disabled')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Integration Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Integration', () => {
|
||||
it('should work in a typical workflow: enable -> click -> disable', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act - Start enabled
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Assert - Can click when enabled
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1)
|
||||
|
||||
// Act - Disable after click (simulating loading state)
|
||||
rerender(<Actions disabled={true} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Cannot click when disabled
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(1) // Still 1, not 2
|
||||
|
||||
// Act - Re-enable
|
||||
rerender(<Actions disabled={false} handleNextStep={handleNextStep} />)
|
||||
|
||||
// Assert - Can click again
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
expect(handleNextStep).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should maintain consistent rendering across multiple state changes', () => {
|
||||
// Arrange
|
||||
const handleNextStep = vi.fn()
|
||||
|
||||
// Act
|
||||
const { rerender } = render(
|
||||
<Actions disabled={false} handleNextStep={handleNextStep} />,
|
||||
)
|
||||
|
||||
// Toggle disabled state multiple times
|
||||
const states = [true, false, true, false, true]
|
||||
states.forEach((disabled) => {
|
||||
rerender(<Actions disabled={disabled} handleNextStep={handleNextStep} />)
|
||||
@ -541,7 +401,6 @@ describe('Actions', () => {
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
})
|
||||
|
||||
// Assert - Button should still render correctly
|
||||
expect(screen.getByRole('button')).toBeInTheDocument()
|
||||
expect(screen.getByText('datasetCreation.stepOne.button')).toBeInTheDocument()
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,41 +5,19 @@ import * as React from 'react'
|
||||
import { BlockEnum, WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { RAG_PIPELINE_PREVIEW_CHUNK_NUM } from '@/config'
|
||||
import { ChunkingMode } from '@/models/datasets'
|
||||
import Result from './index'
|
||||
import ResultPreview from './result-preview'
|
||||
import { formatPreviewChunks } from './result-preview/utils'
|
||||
import Tabs from './tabs'
|
||||
import Tab from './tabs/tab'
|
||||
|
||||
// ============================================================================
|
||||
// Pre-declare variables used in mocks (hoisting)
|
||||
// ============================================================================
|
||||
import Result from '../index'
|
||||
import ResultPreview from '../result-preview'
|
||||
import { formatPreviewChunks } from '../result-preview/utils'
|
||||
import Tabs from '../tabs'
|
||||
import Tab from '../tabs/tab'
|
||||
|
||||
let mockWorkflowRunningData: WorkflowRunningData | undefined
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Dependencies
|
||||
// ============================================================================
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, options?: { ns?: string, count?: number }) => {
|
||||
const ns = options?.ns ? `${options.ns}.` : ''
|
||||
if (options?.count !== undefined)
|
||||
return `${ns}${key} (count: ${options.count})`
|
||||
return `${ns}${key}`
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useStore: <T,>(selector: (state: { workflowRunningData: WorkflowRunningData | undefined }) => T) =>
|
||||
selector({ workflowRunningData: mockWorkflowRunningData }),
|
||||
}))
|
||||
|
||||
// Mock child components
|
||||
vi.mock('@/app/components/workflow/run/result-panel', () => ({
|
||||
default: ({
|
||||
inputs,
|
||||
@ -102,10 +80,6 @@ vi.mock('@/app/components/rag-pipeline/components/chunk-card-list', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Data Factories
|
||||
// ============================================================================
|
||||
|
||||
const createMockWorkflowRunningData = (
|
||||
overrides?: Partial<WorkflowRunningData>,
|
||||
): WorkflowRunningData => ({
|
||||
@ -191,26 +165,15 @@ const createQAChunkOutputs = (qaCount: number = 5) => ({
|
||||
})),
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
const resetAllMocks = () => {
|
||||
mockWorkflowRunningData = undefined
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tab Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Tab', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render tab with label', () => {
|
||||
const mockOnClick = vi.fn()
|
||||
@ -283,9 +246,6 @@ describe('Tab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User Interaction Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should call onClick with value when clicked', () => {
|
||||
const mockOnClick = vi.fn()
|
||||
@ -325,9 +285,6 @@ describe('Tab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should maintain stable handleClick callback reference', () => {
|
||||
const mockOnClick = vi.fn()
|
||||
@ -353,33 +310,26 @@ describe('Tab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Props Variation Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Props Variations', () => {
|
||||
it('should render with all combinations of isActive and workflowRunningData', () => {
|
||||
const mockOnClick = vi.fn()
|
||||
const workflowData = createMockWorkflowRunningData()
|
||||
|
||||
// Active with data
|
||||
const { rerender } = render(
|
||||
<Tab isActive={true} label="Tab" value="tab" workflowRunningData={workflowData} onClick={mockOnClick} />,
|
||||
)
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Inactive with data
|
||||
rerender(
|
||||
<Tab isActive={false} label="Tab" value="tab" workflowRunningData={workflowData} onClick={mockOnClick} />,
|
||||
)
|
||||
expect(screen.getByRole('button')).not.toBeDisabled()
|
||||
|
||||
// Active without data
|
||||
rerender(
|
||||
<Tab isActive={true} label="Tab" value="tab" workflowRunningData={undefined} onClick={mockOnClick} />,
|
||||
)
|
||||
expect(screen.getByRole('button')).toBeDisabled()
|
||||
|
||||
// Inactive without data
|
||||
rerender(
|
||||
<Tab isActive={false} label="Tab" value="tab" workflowRunningData={undefined} onClick={mockOnClick} />,
|
||||
)
|
||||
@ -388,18 +338,11 @@ describe('Tab', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Tabs Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Tabs', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render all three tabs', () => {
|
||||
render(
|
||||
@ -440,18 +383,12 @@ describe('Tabs', () => {
|
||||
)
|
||||
|
||||
const buttons = screen.getAllByRole('button')
|
||||
// RESULT tab
|
||||
expect(buttons[0]).toHaveClass('border-transparent')
|
||||
// DETAIL tab (active)
|
||||
expect(buttons[1]).toHaveClass('border-util-colors-blue-brand-blue-brand-600')
|
||||
// TRACING tab
|
||||
expect(buttons[2]).toHaveClass('border-transparent')
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User Interaction Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should call switchTab when RESULT tab is clicked', () => {
|
||||
const mockSwitchTab = vi.fn()
|
||||
@ -522,9 +459,6 @@ describe('Tabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Props Variation Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Props Variations', () => {
|
||||
it('should handle all currentTab values', () => {
|
||||
const mockSwitchTab = vi.fn()
|
||||
@ -554,14 +488,7 @@ describe('Tabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// formatPreviewChunks Utility Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('formatPreviewChunks', () => {
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should return undefined when outputs is null', () => {
|
||||
expect(formatPreviewChunks(null)).toBeUndefined()
|
||||
@ -581,9 +508,6 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// General Chunks Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('General Chunks (text mode)', () => {
|
||||
it('should format general chunks correctly', () => {
|
||||
const outputs = createGeneralChunkOutputs(3)
|
||||
@ -613,9 +537,6 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Parent-Child Chunks Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Parent-Child Chunks (hierarchical mode)', () => {
|
||||
it('should format paragraph mode chunks correctly', () => {
|
||||
const outputs = createParentChildChunkOutputs('paragraph', 3)
|
||||
@ -678,9 +599,6 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// QA Chunks Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('QA Chunks (qa mode)', () => {
|
||||
it('should format QA chunks correctly', () => {
|
||||
const outputs = createQAChunkOutputs(3)
|
||||
@ -710,18 +628,11 @@ describe('formatPreviewChunks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// ResultPreview Component Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('ResultPreview', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render loading state when isRunning is true and no outputs', () => {
|
||||
render(
|
||||
@ -778,7 +689,7 @@ describe('ResultPreview', () => {
|
||||
)
|
||||
|
||||
expect(
|
||||
screen.getByText(`pipeline.result.resultPreview.footerTip (count: ${RAG_PIPELINE_PREVIEW_CHUNK_NUM})`),
|
||||
screen.getByText(`pipeline.result.resultPreview.footerTip:{"count":${RAG_PIPELINE_PREVIEW_CHUNK_NUM}}`),
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -799,9 +710,6 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// User Interaction Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('User Interactions', () => {
|
||||
it('should call onSwitchToDetail when view details button is clicked', () => {
|
||||
const mockOnSwitchToDetail = vi.fn()
|
||||
@ -821,9 +729,6 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Props Variation Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Props Variations', () => {
|
||||
it('should render with general chunks output', () => {
|
||||
const outputs = createGeneralChunkOutputs(3)
|
||||
@ -874,9 +779,6 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle outputs with no previewChunks result', () => {
|
||||
const outputs = {
|
||||
@ -893,7 +795,6 @@ describe('ResultPreview', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Should not render chunk card list when formatPreviewChunks returns undefined
|
||||
expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -907,14 +808,10 @@ describe('ResultPreview', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Error section should not render when isRunning is true
|
||||
expect(screen.queryByText('pipeline.result.resultPreview.error')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should memoize previewChunks calculation', () => {
|
||||
const outputs = createGeneralChunkOutputs(3)
|
||||
@ -927,7 +824,6 @@ describe('ResultPreview', () => {
|
||||
/>,
|
||||
)
|
||||
|
||||
// Re-render with same outputs - should use memoized value
|
||||
rerender(
|
||||
<ResultPreview
|
||||
isRunning={false}
|
||||
@ -942,19 +838,12 @@ describe('ResultPreview', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Result Component Tests (Main Component)
|
||||
// ============================================================================
|
||||
|
||||
describe('Result', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resetAllMocks()
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render tabs and result preview by default', () => {
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData({
|
||||
@ -967,7 +856,6 @@ describe('Result', () => {
|
||||
|
||||
render(<Result />)
|
||||
|
||||
// Tabs should be rendered
|
||||
expect(screen.getByText('runLog.result')).toBeInTheDocument()
|
||||
expect(screen.getByText('runLog.detail')).toBeInTheDocument()
|
||||
expect(screen.getByText('runLog.tracing')).toBeInTheDocument()
|
||||
@ -1003,9 +891,6 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Tab Switching Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Tab Switching', () => {
|
||||
it('should switch to DETAIL tab when clicked', async () => {
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData()
|
||||
@ -1042,13 +927,11 @@ describe('Result', () => {
|
||||
|
||||
render(<Result />)
|
||||
|
||||
// Switch to DETAIL
|
||||
fireEvent.click(screen.getByText('runLog.detail'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('result-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Switch back to RESULT
|
||||
fireEvent.click(screen.getByText('runLog.result'))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument()
|
||||
@ -1056,9 +939,6 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// DETAIL Tab Content Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('DETAIL Tab Content', () => {
|
||||
it('should render ResultPanel with correct props', async () => {
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData({
|
||||
@ -1109,9 +989,6 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// TRACING Tab Content Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('TRACING Tab Content', () => {
|
||||
it('should render TracingPanel with tracing data', async () => {
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData()
|
||||
@ -1137,15 +1014,11 @@ describe('Result', () => {
|
||||
fireEvent.click(screen.getByText('runLog.tracing'))
|
||||
|
||||
await waitFor(() => {
|
||||
// Both TracingPanel and Loading should be rendered
|
||||
expect(screen.getByTestId('tracing-panel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Switch to Detail from Result Preview Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Switch to Detail from Result Preview', () => {
|
||||
it('should switch to DETAIL tab when onSwitchToDetail is triggered from ResultPreview', async () => {
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData({
|
||||
@ -1159,7 +1032,6 @@ describe('Result', () => {
|
||||
|
||||
render(<Result />)
|
||||
|
||||
// Click the view details button in error state
|
||||
fireEvent.click(screen.getByText('pipeline.result.resultPreview.viewDetails'))
|
||||
|
||||
await waitFor(() => {
|
||||
@ -1168,16 +1040,12 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Edge Cases Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined workflowRunningData', () => {
|
||||
mockWorkflowRunningData = undefined
|
||||
|
||||
render(<Result />)
|
||||
|
||||
// All tabs should be disabled
|
||||
const buttons = screen.getAllByRole('button')
|
||||
buttons.forEach((button) => {
|
||||
expect(button).toBeDisabled()
|
||||
@ -1193,7 +1061,6 @@ describe('Result', () => {
|
||||
|
||||
render(<Result />)
|
||||
|
||||
// Should show loading in RESULT tab (isRunning condition)
|
||||
expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -1223,36 +1090,28 @@ describe('Result', () => {
|
||||
|
||||
render(<Result />)
|
||||
|
||||
// Should show error when stopped
|
||||
expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// State Management Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('State Management', () => {
|
||||
it('should maintain tab state across re-renders', async () => {
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData()
|
||||
|
||||
const { rerender } = render(<Result />)
|
||||
|
||||
// Switch to DETAIL tab
|
||||
fireEvent.click(screen.getByText('runLog.detail'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('result-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
// Re-render component
|
||||
rerender(<Result />)
|
||||
|
||||
// Should still be on DETAIL tab
|
||||
expect(screen.getByTestId('result-panel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render different states based on workflowRunningData', () => {
|
||||
// Test 1: Running state with no outputs
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData({
|
||||
result: {
|
||||
...createMockWorkflowRunningData().result,
|
||||
@ -1265,7 +1124,6 @@ describe('Result', () => {
|
||||
expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument()
|
||||
unmount()
|
||||
|
||||
// Test 2: Completed state with outputs
|
||||
const outputs = createGeneralChunkOutputs(3)
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData({
|
||||
result: {
|
||||
@ -1280,19 +1138,14 @@ describe('Result', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// -------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be memoized', () => {
|
||||
mockWorkflowRunningData = createMockWorkflowRunningData()
|
||||
|
||||
const { rerender } = render(<Result />)
|
||||
|
||||
// Re-render without changes
|
||||
rerender(<Result />)
|
||||
|
||||
// Component should still be rendered correctly
|
||||
expect(screen.getByText('runLog.result')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -3,21 +3,12 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
// ============================================================================
|
||||
// Import Components After Mocks
|
||||
// ============================================================================
|
||||
import RagPipelineHeader from '../index'
|
||||
import InputFieldButton from '../input-field-button'
|
||||
import Publisher from '../publisher'
|
||||
import Popup from '../publisher/popup'
|
||||
import RunMode from '../run-mode'
|
||||
|
||||
import RagPipelineHeader from './index'
|
||||
import InputFieldButton from './input-field-button'
|
||||
import Publisher from './publisher'
|
||||
import Popup from './publisher/popup'
|
||||
import RunMode from './run-mode'
|
||||
|
||||
// ============================================================================
|
||||
// Mock External Dependencies
|
||||
// ============================================================================
|
||||
|
||||
// Mock workflow store
|
||||
const mockSetShowInputFieldPanel = vi.fn()
|
||||
const mockSetShowEnvPanel = vi.fn()
|
||||
const mockSetIsPreparingDataSource = vi.fn()
|
||||
@ -51,7 +42,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow hooks
|
||||
const mockHandleSyncWorkflowDraft = vi.fn()
|
||||
const mockHandleCheckBeforePublish = vi.fn().mockResolvedValue(true)
|
||||
const mockHandleStopRun = vi.fn()
|
||||
@ -72,7 +62,6 @@ vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock Header component
|
||||
vi.mock('@/app/components/workflow/header', () => ({
|
||||
default: ({ normal, viewHistory }: {
|
||||
normal?: { components?: { left?: ReactNode, middle?: ReactNode }, runAndHistoryProps?: unknown }
|
||||
@ -87,21 +76,18 @@ vi.mock('@/app/components/workflow/header', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock next/navigation
|
||||
const mockPush = vi.fn()
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'test-dataset-id' }),
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}))
|
||||
|
||||
// Mock next/link
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, ...props }: PropsWithChildren<{ href: string }>) => (
|
||||
<a href={href} {...props}>{children}</a>
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock service hooks
|
||||
const mockPublishWorkflow = vi.fn().mockResolvedValue({ created_at: Date.now() })
|
||||
const mockPublishAsCustomizedPipeline = vi.fn().mockResolvedValue({})
|
||||
|
||||
@ -127,7 +113,6 @@ vi.mock('@/service/knowledge/use-dataset', () => ({
|
||||
useInvalidDatasetList: () => vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock context hooks
|
||||
const mockMutateDatasetRes = vi.fn()
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: () => mockMutateDatasetRes,
|
||||
@ -145,7 +130,6 @@ vi.mock('@/context/provider-context', () => ({
|
||||
selector(mockProviderContextValue),
|
||||
}))
|
||||
|
||||
// Mock event emitter context
|
||||
const mockEventEmitter = {
|
||||
useSubscription: vi.fn(),
|
||||
}
|
||||
@ -156,7 +140,6 @@ vi.mock('@/context/event-emitter', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock hooks
|
||||
vi.mock('@/hooks/use-api-access-url', () => ({
|
||||
useDatasetApiAccessUrl: () => '/api/docs',
|
||||
}))
|
||||
@ -167,12 +150,10 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock amplitude tracking
|
||||
vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock toast context
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
@ -180,13 +161,11 @@ vi.mock('@/app/components/base/toast', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow utils
|
||||
vi.mock('@/app/components/workflow/utils', () => ({
|
||||
getKeyboardKeyCodeBySystem: (key: string) => key,
|
||||
getKeyboardKeyNameBySystem: (key: string) => key,
|
||||
}))
|
||||
|
||||
// Mock ahooks
|
||||
vi.mock('ahooks', () => ({
|
||||
useBoolean: (initial: boolean) => {
|
||||
let value = initial
|
||||
@ -202,7 +181,6 @@ vi.mock('ahooks', () => ({
|
||||
useKeyPress: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock portal components - keep actual behavior for open state
|
||||
let portalOpenState = false
|
||||
vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: PropsWithChildren<{
|
||||
@ -224,8 +202,7 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock PublishAsKnowledgePipelineModal
|
||||
vi.mock('../../publish-as-knowledge-pipeline-modal', () => ({
|
||||
vi.mock('../../../publish-as-knowledge-pipeline-modal', () => ({
|
||||
default: ({ onConfirm, onCancel }: {
|
||||
onConfirm: (name: string, icon: unknown, description?: string) => void
|
||||
onCancel: () => void
|
||||
@ -238,10 +215,6 @@ vi.mock('../../publish-as-knowledge-pipeline-modal', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test Suites
|
||||
// ============================================================================
|
||||
|
||||
describe('RagPipelineHeader', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -259,9 +232,6 @@ describe('RagPipelineHeader', () => {
|
||||
mockProviderContextValue = createMockProviderContextValue()
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
render(<RagPipelineHeader />)
|
||||
@ -286,19 +256,14 @@ describe('RagPipelineHeader', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should compute viewHistoryProps based on pipelineId', () => {
|
||||
// Test with first pipelineId
|
||||
mockStoreState.pipelineId = 'pipeline-alpha'
|
||||
const { unmount } = render(<RagPipelineHeader />)
|
||||
let viewHistoryContent = screen.getByTestId('header-view-history').textContent
|
||||
expect(viewHistoryContent).toContain('pipeline-alpha')
|
||||
unmount()
|
||||
|
||||
// Test with different pipelineId
|
||||
mockStoreState.pipelineId = 'pipeline-beta'
|
||||
render(<RagPipelineHeader />)
|
||||
viewHistoryContent = screen.getByTestId('header-view-history').textContent
|
||||
@ -320,9 +285,6 @@ describe('InputFieldButton', () => {
|
||||
mockStoreState.setShowEnvPanel = mockSetShowEnvPanel
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render button with correct text', () => {
|
||||
render(<InputFieldButton />)
|
||||
@ -337,9 +299,6 @@ describe('InputFieldButton', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Event Handler Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Event Handlers', () => {
|
||||
it('should call setShowInputFieldPanel(true) when clicked', () => {
|
||||
render(<InputFieldButton />)
|
||||
@ -367,16 +326,12 @@ describe('InputFieldButton', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Edge Cases
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle undefined setShowInputFieldPanel gracefully', () => {
|
||||
mockStoreState.setShowInputFieldPanel = undefined as unknown as typeof mockSetShowInputFieldPanel
|
||||
|
||||
render(<InputFieldButton />)
|
||||
|
||||
// Should not throw when clicked
|
||||
expect(() => fireEvent.click(screen.getByRole('button'))).not.toThrow()
|
||||
})
|
||||
})
|
||||
@ -388,9 +343,6 @@ describe('Publisher', () => {
|
||||
portalOpenState = false
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render publish button', () => {
|
||||
render(<Publisher />)
|
||||
@ -410,9 +362,6 @@ describe('Publisher', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Interaction Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Interactions', () => {
|
||||
it('should call handleSyncWorkflowDraft when opening', () => {
|
||||
render(<Publisher />)
|
||||
@ -430,7 +379,6 @@ describe('Publisher', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('portal-trigger'))
|
||||
|
||||
// After click, handleOpenChange should be called
|
||||
expect(mockHandleSyncWorkflowDraft).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -447,9 +395,6 @@ describe('Popup', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render popup container', () => {
|
||||
render(<Popup />)
|
||||
@ -475,7 +420,6 @@ describe('Popup', () => {
|
||||
it('should render keyboard shortcuts', () => {
|
||||
render(<Popup />)
|
||||
|
||||
// Should show the keyboard shortcut keys
|
||||
expect(screen.getByText('ctrl')).toBeInTheDocument()
|
||||
expect(screen.getByText('⇧')).toBeInTheDocument()
|
||||
expect(screen.getByText('P')).toBeInTheDocument()
|
||||
@ -500,9 +444,6 @@ describe('Popup', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Button State Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Button States', () => {
|
||||
it('should disable goToAddDocuments when not published', () => {
|
||||
mockStoreState.publishedAt = 0
|
||||
@ -532,9 +473,6 @@ describe('Popup', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Premium Badge Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Premium Badge', () => {
|
||||
it('should show premium badge when not allowed to publish as template', () => {
|
||||
mockProviderContextValue = createMockProviderContextValue({
|
||||
@ -557,9 +495,6 @@ describe('Popup', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Interaction Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Interactions', () => {
|
||||
it('should call handleCheckBeforePublish when publish button clicked', async () => {
|
||||
render(<Popup />)
|
||||
@ -598,9 +533,6 @@ describe('Popup', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Auto-save Display Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Auto-save Display', () => {
|
||||
it('should show auto-saved time when not published', () => {
|
||||
mockStoreState.publishedAt = 0
|
||||
@ -629,9 +561,6 @@ describe('RunMode', () => {
|
||||
mockEventEmitterEnabled = true
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Rendering Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Rendering', () => {
|
||||
it('should render run button with default text', () => {
|
||||
render(<RunMode />)
|
||||
@ -654,9 +583,6 @@ describe('RunMode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Running State Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Running States', () => {
|
||||
it('should show processing state when running', () => {
|
||||
mockStoreState.workflowRunningData = {
|
||||
@ -677,7 +603,6 @@ describe('RunMode', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// There should be two buttons: run button and stop button
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBe(2)
|
||||
})
|
||||
@ -751,7 +676,6 @@ describe('RunMode', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Should only have one button (run button)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
expect(buttons.length).toBe(1)
|
||||
})
|
||||
@ -781,9 +705,6 @@ describe('RunMode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Disabled State Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Disabled States', () => {
|
||||
it('should be disabled when running', () => {
|
||||
mockStoreState.workflowRunningData = {
|
||||
@ -818,9 +739,6 @@ describe('RunMode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Interaction Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Interactions', () => {
|
||||
it('should call handleWorkflowStartRunInWorkflow when clicked', () => {
|
||||
render(<RunMode />)
|
||||
@ -838,7 +756,6 @@ describe('RunMode', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Click the stop button (second button)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[1])
|
||||
|
||||
@ -850,7 +767,6 @@ describe('RunMode', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Click the cancel button (second button)
|
||||
const buttons = screen.getAllByRole('button')
|
||||
fireEvent.click(buttons[1])
|
||||
|
||||
@ -883,14 +799,10 @@ describe('RunMode', () => {
|
||||
const runButton = screen.getAllByRole('button')[0]
|
||||
fireEvent.click(runButton)
|
||||
|
||||
// Should not be called because button is disabled
|
||||
expect(mockHandleWorkflowStartRunInWorkflow).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Event Emitter Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Event Emitter', () => {
|
||||
it('should subscribe to event emitter', () => {
|
||||
render(<RunMode />)
|
||||
@ -904,7 +816,6 @@ describe('RunMode', () => {
|
||||
result: { status: WorkflowRunningStatus.Running },
|
||||
}
|
||||
|
||||
// Capture the subscription callback
|
||||
let subscriptionCallback: ((v: { type: string }) => void) | null = null
|
||||
mockEventEmitter.useSubscription.mockImplementation((callback: (v: { type: string }) => void) => {
|
||||
subscriptionCallback = callback
|
||||
@ -912,7 +823,6 @@ describe('RunMode', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Simulate the EVENT_WORKFLOW_STOP event (actual value is 'WORKFLOW_STOP')
|
||||
expect(subscriptionCallback).not.toBeNull()
|
||||
subscriptionCallback!({ type: 'WORKFLOW_STOP' })
|
||||
|
||||
@ -932,7 +842,6 @@ describe('RunMode', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Simulate a different event type
|
||||
subscriptionCallback!({ type: 'some_other_event' })
|
||||
|
||||
expect(mockHandleStopRun).not.toHaveBeenCalled()
|
||||
@ -941,7 +850,6 @@ describe('RunMode', () => {
|
||||
it('should handle undefined eventEmitter gracefully', () => {
|
||||
mockEventEmitterEnabled = false
|
||||
|
||||
// Should not throw when eventEmitter is undefined
|
||||
expect(() => render(<RunMode />)).not.toThrow()
|
||||
})
|
||||
|
||||
@ -951,14 +859,10 @@ describe('RunMode', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// useSubscription should not be called
|
||||
expect(mockEventEmitter.useSubscription).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Style Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Styles', () => {
|
||||
it('should have rounded-md class when not disabled', () => {
|
||||
render(<RunMode />)
|
||||
@ -1053,21 +957,13 @@ describe('RunMode', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Memoization Tests
|
||||
// --------------------------------------------------------------------------
|
||||
describe('Memoization', () => {
|
||||
it('should be wrapped in React.memo', () => {
|
||||
// RunMode is exported as default from run-mode.tsx with React.memo
|
||||
// We can verify it's memoized by checking the component's $$typeof symbol
|
||||
expect((RunMode as unknown as { $$typeof: symbol }).$$typeof).toBe(Symbol.for('react.memo'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Integration Tests
|
||||
// ============================================================================
|
||||
describe('Integration', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1087,10 +983,8 @@ describe('Integration', () => {
|
||||
it('should render all child components in RagPipelineHeader', () => {
|
||||
render(<RagPipelineHeader />)
|
||||
|
||||
// InputFieldButton
|
||||
expect(screen.getByText(/inputField/i)).toBeInTheDocument()
|
||||
|
||||
// Publisher (via header-middle slot)
|
||||
expect(screen.getByTestId('header-middle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -1104,9 +998,6 @@ describe('Integration', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// Edge Cases
|
||||
// ============================================================================
|
||||
describe('Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1136,20 +1027,17 @@ describe('Edge Cases', () => {
|
||||
result: undefined as unknown as { status: WorkflowRunningStatus },
|
||||
}
|
||||
|
||||
// Component will crash when accessing result.status - this documents current behavior
|
||||
expect(() => render(<RunMode />)).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('RunMode Edge Cases', () => {
|
||||
beforeEach(() => {
|
||||
// Ensure clean state for each test
|
||||
mockStoreState.workflowRunningData = null
|
||||
mockStoreState.isPreparingDataSource = false
|
||||
})
|
||||
|
||||
it('should handle both isPreparingDataSource and isRunning being true', () => {
|
||||
// This shouldn't happen in practice, but test the priority
|
||||
mockStoreState.isPreparingDataSource = true
|
||||
mockStoreState.workflowRunningData = {
|
||||
task_id: 'task-123',
|
||||
@ -1158,7 +1046,6 @@ describe('Edge Cases', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Button should be disabled
|
||||
const runButton = screen.getAllByRole('button')[0]
|
||||
expect(runButton).toBeDisabled()
|
||||
})
|
||||
@ -1169,7 +1056,6 @@ describe('Edge Cases', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Verify the button is enabled and shows testRun text
|
||||
const button = screen.getByRole('button')
|
||||
expect(button).not.toBeDisabled()
|
||||
expect(button.textContent).toContain('pipeline.common.testRun')
|
||||
@ -1193,7 +1079,6 @@ describe('Edge Cases', () => {
|
||||
|
||||
render(<RunMode text="Start Pipeline" />)
|
||||
|
||||
// Should show reRun, not custom text
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.textContent).toContain('pipeline.common.reRun')
|
||||
expect(screen.queryByText('Start Pipeline')).not.toBeInTheDocument()
|
||||
@ -1205,7 +1090,6 @@ describe('Edge Cases', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Verify keyboard shortcut elements exist
|
||||
expect(screen.getByText('alt')).toBeInTheDocument()
|
||||
expect(screen.getByText('R')).toBeInTheDocument()
|
||||
})
|
||||
@ -1216,7 +1100,6 @@ describe('Edge Cases', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Should have svg icon in the button
|
||||
const button = screen.getByRole('button')
|
||||
expect(button.querySelector('svg')).toBeInTheDocument()
|
||||
})
|
||||
@ -1229,7 +1112,6 @@ describe('Edge Cases', () => {
|
||||
|
||||
render(<RunMode />)
|
||||
|
||||
// Should have animate-spin class on the loader icon
|
||||
const runButton = screen.getAllByRole('button')[0]
|
||||
const spinningIcon = runButton.querySelector('.animate-spin')
|
||||
expect(spinningIcon).toBeInTheDocument()
|
||||
@ -1252,7 +1134,6 @@ describe('Edge Cases', () => {
|
||||
|
||||
render(<Popup />)
|
||||
|
||||
// Should render without crashing
|
||||
expect(screen.getByText(/workflow.common.autoSaved/i)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import RunMode from './run-mode'
|
||||
import RunMode from '../run-mode'
|
||||
|
||||
const mockHandleWorkflowStartRunInWorkflow = vi.fn()
|
||||
const mockHandleStopRun = vi.fn()
|
||||
@ -10,13 +10,6 @@ const mockSetShowDebugAndPreviewPanel = vi.fn()
|
||||
|
||||
let mockWorkflowRunningData: { task_id: string, result: { status: string } } | undefined
|
||||
let mockIsPreparingDataSource = false
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowRun: () => ({
|
||||
handleStopRun: mockHandleStopRun,
|
||||
@ -86,7 +79,7 @@ describe('RunMode', () => {
|
||||
it('should render test run text when no data', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.testRun')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.testRun')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render custom text when provided', () => {
|
||||
@ -110,7 +103,7 @@ describe('RunMode', () => {
|
||||
it('should call start run when button clicked', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.testRun'))
|
||||
fireEvent.click(screen.getByText('pipeline.common.testRun'))
|
||||
|
||||
expect(mockHandleWorkflowStartRunInWorkflow).toHaveBeenCalled()
|
||||
})
|
||||
@ -127,7 +120,7 @@ describe('RunMode', () => {
|
||||
it('should show processing text', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.processing')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.processing')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show stop button', () => {
|
||||
@ -139,7 +132,7 @@ describe('RunMode', () => {
|
||||
it('should disable run button', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
const button = screen.getByText('common.processing').closest('button')
|
||||
const button = screen.getByText('pipeline.common.processing').closest('button')
|
||||
expect(button).toBeDisabled()
|
||||
})
|
||||
|
||||
@ -160,7 +153,7 @@ describe('RunMode', () => {
|
||||
}
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.reRun')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.reRun')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -172,7 +165,7 @@ describe('RunMode', () => {
|
||||
it('should show preparing text', () => {
|
||||
render(<RunMode />)
|
||||
|
||||
expect(screen.getByText('common.preparingDataSource')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.preparingDataSource')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show database icon', () => {
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import Popup from './popup'
|
||||
import Popup from '../popup'
|
||||
|
||||
const mockPublishWorkflow = vi.fn().mockResolvedValue({ created_at: '2024-01-01T00:00:00Z' })
|
||||
const mockPublishAsCustomizedPipeline = vi.fn().mockResolvedValue({})
|
||||
@ -19,14 +19,6 @@ let mockPublishedAt: string | undefined = '2024-01-01T00:00:00Z'
|
||||
let mockDraftUpdatedAt: string | undefined = '2024-06-01T00:00:00Z'
|
||||
let mockPipelineId: string | undefined = 'pipeline-123'
|
||||
let mockIsAllowPublishAsCustom = true
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
Trans: ({ i18nKey }: { i18nKey: string }) => <span>{i18nKey}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('next/navigation', () => ({
|
||||
useParams: () => ({ datasetId: 'ds-123' }),
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
@ -40,7 +32,6 @@ vi.mock('next/link', () => ({
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useBoolean: (initial: boolean) => {
|
||||
// Simple implementation for testing
|
||||
const state = { value: initial }
|
||||
return [state.value, {
|
||||
setFalse: vi.fn(),
|
||||
@ -183,7 +174,7 @@ vi.mock('@/utils/classnames', () => ({
|
||||
cn: (...args: string[]) => args.filter(Boolean).join(' '),
|
||||
}))
|
||||
|
||||
vi.mock('../../publish-as-knowledge-pipeline-modal', () => ({
|
||||
vi.mock('../../../publish-as-knowledge-pipeline-modal', () => ({
|
||||
default: ({ onConfirm, onCancel }: { onConfirm: (name: string, icon: unknown, desc: string) => void, onCancel: () => void }) => (
|
||||
<div data-testid="publish-as-modal">
|
||||
<button data-testid="publish-as-confirm" onClick={() => onConfirm('My Pipeline', { icon_type: 'emoji' }, 'desc')}>
|
||||
@ -218,41 +209,41 @@ describe('Popup', () => {
|
||||
it('should render when published', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.latestPublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/common.publishedAt/)).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.latestPublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/workflow\.common\.publishedAt/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render unpublished state', () => {
|
||||
mockPublishedAt = undefined
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.currentDraftUnpublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/common.autoSaved/)).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.currentDraftUnpublished')).toBeInTheDocument()
|
||||
expect(screen.getByText(/workflow\.common\.autoSaved/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render publish button with shortcuts', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.publishUpdate')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.publishUpdate')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('shortcuts')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "Go to Add Documents" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.goToAddDocuments')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.goToAddDocuments')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "API Reference" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.accessAPIReference')).toBeInTheDocument()
|
||||
expect(screen.getByText('workflow.common.accessAPIReference')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render "Publish As" button', () => {
|
||||
render(<Popup />)
|
||||
|
||||
expect(screen.getByText('common.publishAs')).toBeInTheDocument()
|
||||
expect(screen.getByText('pipeline.common.publishAs')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -276,7 +267,7 @@ describe('Popup', () => {
|
||||
it('should navigate to add documents page', () => {
|
||||
render(<Popup />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.goToAddDocuments'))
|
||||
fireEvent.click(screen.getByText('pipeline.common.goToAddDocuments'))
|
||||
|
||||
expect(mockPush).toHaveBeenCalledWith('/datasets/ds-123/documents/create-from-pipeline')
|
||||
})
|
||||
@ -287,7 +278,7 @@ describe('Popup', () => {
|
||||
mockPublishedAt = undefined
|
||||
render(<Popup />)
|
||||
|
||||
const btn = screen.getByText('common.goToAddDocuments').closest('button')
|
||||
const btn = screen.getByText('pipeline.common.goToAddDocuments').closest('button')
|
||||
expect(btn).toBeDisabled()
|
||||
})
|
||||
|
||||
@ -295,7 +286,7 @@ describe('Popup', () => {
|
||||
mockPublishedAt = undefined
|
||||
render(<Popup />)
|
||||
|
||||
const btn = screen.getByText('common.publishAs').closest('button')
|
||||
const btn = screen.getByText('pipeline.common.publishAs').closest('button')
|
||||
expect(btn).toBeDisabled()
|
||||
})
|
||||
})
|
||||
@ -305,7 +296,7 @@ describe('Popup', () => {
|
||||
mockIsAllowPublishAsCustom = false
|
||||
render(<Popup />)
|
||||
|
||||
fireEvent.click(screen.getByText('common.publishAs'))
|
||||
fireEvent.click(screen.getByText('pipeline.common.publishAs'))
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
})
|
||||
@ -6,10 +6,6 @@ import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { FlowType } from '@/types/common'
|
||||
|
||||
// ============================================================================
|
||||
// Import hooks after mocks
|
||||
// ============================================================================
|
||||
|
||||
import {
|
||||
useAvailableNodesMetaData,
|
||||
useDSL,
|
||||
@ -20,16 +16,11 @@ import {
|
||||
usePipelineRefreshDraft,
|
||||
usePipelineRun,
|
||||
usePipelineStartRun,
|
||||
} from './index'
|
||||
import { useConfigsMap } from './use-configs-map'
|
||||
import { useConfigurations, useInitialData } from './use-input-fields'
|
||||
import { usePipelineTemplate } from './use-pipeline-template'
|
||||
} from '../index'
|
||||
import { useConfigsMap } from '../use-configs-map'
|
||||
import { useConfigurations, useInitialData } from '../use-input-fields'
|
||||
import { usePipelineTemplate } from '../use-pipeline-template'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock the workflow store
|
||||
const _mockGetState = vi.fn()
|
||||
const mockUseStore = vi.fn()
|
||||
const mockUseWorkflowStore = vi.fn()
|
||||
@ -39,14 +30,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => mockUseWorkflowStore(),
|
||||
}))
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock toast context
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
@ -54,7 +37,6 @@ vi.mock('@/app/components/base/toast', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock event emitter context
|
||||
const mockEventEmit = vi.fn()
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: () => ({
|
||||
@ -64,19 +46,16 @@ vi.mock('@/context/event-emitter', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock i18n docLink
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
|
||||
}))
|
||||
|
||||
// Mock workflow constants
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
||||
WORKFLOW_DATA_UPDATE: 'WORKFLOW_DATA_UPDATE',
|
||||
START_INITIAL_POSITION: { x: 100, y: 100 },
|
||||
}))
|
||||
|
||||
// Mock workflow constants/node
|
||||
vi.mock('@/app/components/workflow/constants/node', () => ({
|
||||
WORKFLOW_COMMON_NODES: [
|
||||
{
|
||||
@ -90,7 +69,6 @@ vi.mock('@/app/components/workflow/constants/node', () => ({
|
||||
],
|
||||
}))
|
||||
|
||||
// Mock data source defaults
|
||||
vi.mock('@/app/components/workflow/nodes/data-source-empty/default', () => ({
|
||||
default: {
|
||||
metaData: { type: BlockEnum.DataSourceEmpty },
|
||||
@ -112,7 +90,6 @@ vi.mock('@/app/components/workflow/nodes/knowledge-base/default', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock workflow utils with all needed exports
|
||||
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal() as Record<string, unknown>
|
||||
return {
|
||||
@ -123,7 +100,6 @@ vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Mock pipeline service
|
||||
const mockExportPipelineConfig = vi.fn()
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useExportPipelineDSL: () => ({
|
||||
@ -131,7 +107,6 @@ vi.mock('@/service/use-pipeline', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow service
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: vi.fn().mockResolvedValue({
|
||||
graph: { nodes: [], edges: [], viewport: {} },
|
||||
@ -139,10 +114,6 @@ vi.mock('@/service/workflow', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useConfigsMap', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -1,8 +1,7 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useDSL } from './use-DSL'
|
||||
import { useDSL } from '../use-DSL'
|
||||
|
||||
// Mock dependencies
|
||||
const mockNotify = vi.fn()
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({ notify: mockNotify }),
|
||||
@ -14,7 +13,7 @@ vi.mock('@/context/event-emitter', () => ({
|
||||
}))
|
||||
|
||||
const mockDoSyncWorkflowDraft = vi.fn()
|
||||
vi.mock('./use-nodes-sync-draft', () => ({
|
||||
vi.mock('../use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({ doSyncWorkflowDraft: mockDoSyncWorkflowDraft }),
|
||||
}))
|
||||
|
||||
@ -37,21 +36,10 @@ const mockDownloadBlob = vi.fn()
|
||||
vi.mock('@/utils/download', () => ({
|
||||
downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
DSL_EXPORT_CHECK: 'DSL_EXPORT_CHECK',
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useDSL', () => {
|
||||
let mockLink: { href: string, download: string, click: ReturnType<typeof vi.fn>, style: { display: string }, remove: ReturnType<typeof vi.fn> }
|
||||
let originalCreateElement: typeof document.createElement
|
||||
@ -62,7 +50,6 @@ describe('useDSL', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create a proper mock link element with all required properties for downloadBlob
|
||||
mockLink = {
|
||||
href: '',
|
||||
download: '',
|
||||
@ -71,7 +58,6 @@ describe('useDSL', () => {
|
||||
remove: vi.fn(),
|
||||
}
|
||||
|
||||
// Save original and mock selectively - only intercept 'a' elements
|
||||
originalCreateElement = document.createElement.bind(document)
|
||||
document.createElement = vi.fn((tagName: string) => {
|
||||
if (tagName === 'a') {
|
||||
@ -80,15 +66,12 @@ describe('useDSL', () => {
|
||||
return originalCreateElement(tagName)
|
||||
}) as typeof document.createElement
|
||||
|
||||
// Mock document.body.appendChild for downloadBlob
|
||||
originalAppendChild = document.body.appendChild.bind(document.body)
|
||||
document.body.appendChild = vi.fn(<T extends Node>(node: T): T => node) as typeof document.body.appendChild
|
||||
|
||||
// downloadBlob uses window.URL, not URL
|
||||
mockCreateObjectURL = vi.spyOn(window.URL, 'createObjectURL').mockReturnValue('blob:test-url')
|
||||
mockRevokeObjectURL = vi.spyOn(window.URL, 'revokeObjectURL').mockImplementation(() => {})
|
||||
|
||||
// Default store state
|
||||
mockGetState.mockReturnValue({
|
||||
pipelineId: 'test-pipeline-id',
|
||||
knowledgeName: 'Test Knowledge Base',
|
||||
@ -170,7 +153,7 @@ describe('useDSL', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'exportFailed',
|
||||
message: 'app.exportFailed',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -251,7 +234,7 @@ describe('useDSL', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'exportFailed',
|
||||
message: 'app.exportFailed',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,13 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useAvailableNodesMetaData } from './use-available-nodes-meta-data'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { useAvailableNodesMetaData } from '../use-available-nodes-meta-data'
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path?: string) => `https://docs.dify.ai${path || ''}`,
|
||||
@ -86,8 +80,8 @@ describe('useAvailableNodesMetaData', () => {
|
||||
const { result } = renderHook(() => useAvailableNodesMetaData())
|
||||
|
||||
result.current.nodes.forEach((node) => {
|
||||
expect(node.metaData.title).toMatch(/^blocks\./)
|
||||
expect(node.metaData.description).toMatch(/^blocksAbout\./)
|
||||
expect(node.metaData.title).toMatch(/^workflow\.blocks\./)
|
||||
expect(node.metaData.description).toMatch(/^workflow\.blocksAbout\./)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useConfigsMap } from './use-configs-map'
|
||||
import { useConfigsMap } from '../use-configs-map'
|
||||
|
||||
const mockPipelineId = 'pipeline-xyz'
|
||||
const mockFileUploadConfig = { max_size: 10 }
|
||||
@ -1,7 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useGetRunAndTraceUrl } from './use-get-run-and-trace-url'
|
||||
import { useGetRunAndTraceUrl } from '../use-get-run-and-trace-url'
|
||||
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
@ -1,7 +1,7 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useInputFieldPanel } from './use-input-field-panel'
|
||||
import { useInputFieldPanel } from '../use-input-field-panel'
|
||||
|
||||
const mockSetShowInputFieldPanel = vi.fn()
|
||||
const mockSetShowInputFieldPreviewPanel = vi.fn()
|
||||
@ -2,7 +2,7 @@ import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { BaseFieldType } from '@/app/components/base/form/form-scenarios/base/types'
|
||||
import { useConfigurations, useInitialData } from './use-input-fields'
|
||||
import { useConfigurations, useInitialData } from '../use-input-fields'
|
||||
|
||||
vi.mock('@/models/pipeline', () => ({
|
||||
VAR_TYPE_MAP: {
|
||||
@ -2,17 +2,8 @@ import { renderHook } from '@testing-library/react'
|
||||
import { act } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// ============================================================================
|
||||
// Import after mocks
|
||||
// ============================================================================
|
||||
import { useNodesSyncDraft } from '../use-nodes-sync-draft'
|
||||
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock reactflow
|
||||
const mockGetNodes = vi.fn()
|
||||
const mockStoreGetState = vi.fn()
|
||||
|
||||
@ -22,7 +13,6 @@ vi.mock('reactflow', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
const mockWorkflowStoreGetState = vi.fn()
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
@ -30,7 +20,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useNodesReadOnly
|
||||
const mockGetNodesReadOnly = vi.fn()
|
||||
vi.mock('@/app/components/workflow/hooks/use-workflow', () => ({
|
||||
useNodesReadOnly: () => ({
|
||||
@ -38,7 +27,6 @@ vi.mock('@/app/components/workflow/hooks/use-workflow', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useSerialAsyncCallback - must pass through arguments
|
||||
vi.mock('@/app/components/workflow/hooks/use-serial-async-callback', () => ({
|
||||
useSerialAsyncCallback: (fn: (...args: unknown[]) => Promise<void>, checkFn: () => boolean) => {
|
||||
return (...args: unknown[]) => {
|
||||
@ -49,13 +37,11 @@ vi.mock('@/app/components/workflow/hooks/use-serial-async-callback', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock service
|
||||
const mockSyncWorkflowDraft = vi.fn()
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
syncWorkflowDraft: (params: unknown) => mockSyncWorkflowDraft(params),
|
||||
}))
|
||||
|
||||
// Mock usePipelineRefreshDraft
|
||||
const mockHandleRefreshWorkflowDraft = vi.fn()
|
||||
vi.mock('@/app/components/rag-pipeline/hooks', () => ({
|
||||
usePipelineRefreshDraft: () => ({
|
||||
@ -63,26 +49,19 @@ vi.mock('@/app/components/rag-pipeline/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock API_PREFIX
|
||||
vi.mock('@/config', () => ({
|
||||
API_PREFIX: '/api',
|
||||
}))
|
||||
|
||||
// Mock postWithKeepalive from service/fetch
|
||||
const mockPostWithKeepalive = vi.fn()
|
||||
vi.mock('@/service/fetch', () => ({
|
||||
postWithKeepalive: (...args: unknown[]) => mockPostWithKeepalive(...args),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useNodesSyncDraft', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Default store state
|
||||
mockStoreGetState.mockReturnValue({
|
||||
getNodes: mockGetNodes,
|
||||
edges: [],
|
||||
@ -204,7 +183,6 @@ describe('useNodesSyncDraft', () => {
|
||||
result.current.syncWorkflowDraftWhenPageClose()
|
||||
})
|
||||
|
||||
// Should not call postWithKeepalive because after filtering temp nodes, array is empty
|
||||
expect(mockPostWithKeepalive).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -347,7 +325,6 @@ describe('useNodesSyncDraft', () => {
|
||||
await result.current.doSyncWorkflowDraft(false)
|
||||
})
|
||||
|
||||
// Wait for json to be called
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
expect(mockHandleRefreshWorkflowDraft).toHaveBeenCalled()
|
||||
@ -371,7 +348,6 @@ describe('useNodesSyncDraft', () => {
|
||||
await result.current.doSyncWorkflowDraft(true)
|
||||
})
|
||||
|
||||
// Wait for json to be called
|
||||
await new Promise(resolve => setTimeout(resolve, 0))
|
||||
|
||||
expect(mockHandleRefreshWorkflowDraft).not.toHaveBeenCalled()
|
||||
@ -1,17 +1,8 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// ============================================================================
|
||||
// Import after mocks
|
||||
// ============================================================================
|
||||
import { usePipelineConfig } from '../use-pipeline-config'
|
||||
|
||||
import { usePipelineConfig } from './use-pipeline-config'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock workflow store
|
||||
const mockUseStore = vi.fn()
|
||||
const mockWorkflowStoreGetState = vi.fn()
|
||||
|
||||
@ -22,27 +13,20 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useWorkflowConfig
|
||||
const mockUseWorkflowConfig = vi.fn()
|
||||
vi.mock('@/service/use-workflow', () => ({
|
||||
useWorkflowConfig: (url: string, callback: (data: unknown) => void) => mockUseWorkflowConfig(url, callback),
|
||||
}))
|
||||
|
||||
// Mock useDataSourceList
|
||||
const mockUseDataSourceList = vi.fn()
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useDataSourceList: (enabled: boolean, callback: (data: unknown) => void) => mockUseDataSourceList(enabled, callback),
|
||||
}))
|
||||
|
||||
// Mock basePath
|
||||
vi.mock('@/utils/var', () => ({
|
||||
basePath: '/base',
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('usePipelineConfig', () => {
|
||||
const mockSetNodesDefaultConfigs = vi.fn()
|
||||
const mockSetPublishedAt = vi.fn()
|
||||
@ -239,7 +223,6 @@ describe('usePipelineConfig', () => {
|
||||
|
||||
capturedCallback?.(dataSourceList)
|
||||
|
||||
// The callback modifies the array in place
|
||||
expect(dataSourceList[0].declaration.identity.icon).toBe('/base/icon.png')
|
||||
})
|
||||
|
||||
@ -274,7 +257,6 @@ describe('usePipelineConfig', () => {
|
||||
|
||||
capturedCallback?.(dataSourceList)
|
||||
|
||||
// Should not modify object icon
|
||||
expect(dataSourceList[0].declaration.identity.icon).toEqual({ url: '/icon.png' })
|
||||
})
|
||||
})
|
||||
@ -1,17 +1,8 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// ============================================================================
|
||||
// Import after mocks
|
||||
// ============================================================================
|
||||
import { usePipelineInit } from '../use-pipeline-init'
|
||||
|
||||
import { usePipelineInit } from './use-pipeline-init'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock workflow store
|
||||
const mockWorkflowStoreGetState = vi.fn()
|
||||
const mockWorkflowStoreSetState = vi.fn()
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
@ -21,14 +12,12 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock dataset detail context
|
||||
const mockUseDatasetDetailContextWithSelector = vi.fn()
|
||||
vi.mock('@/context/dataset-detail', () => ({
|
||||
useDatasetDetailContextWithSelector: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
mockUseDatasetDetailContextWithSelector(selector),
|
||||
}))
|
||||
|
||||
// Mock workflow service
|
||||
const mockFetchWorkflowDraft = vi.fn()
|
||||
const mockSyncWorkflowDraft = vi.fn()
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
@ -36,23 +25,17 @@ vi.mock('@/service/workflow', () => ({
|
||||
syncWorkflowDraft: (params: unknown) => mockSyncWorkflowDraft(params),
|
||||
}))
|
||||
|
||||
// Mock usePipelineConfig
|
||||
vi.mock('./use-pipeline-config', () => ({
|
||||
vi.mock('../use-pipeline-config', () => ({
|
||||
usePipelineConfig: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock usePipelineTemplate
|
||||
vi.mock('./use-pipeline-template', () => ({
|
||||
vi.mock('../use-pipeline-template', () => ({
|
||||
usePipelineTemplate: () => ({
|
||||
nodes: [{ id: 'template-node' }],
|
||||
edges: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('usePipelineInit', () => {
|
||||
const mockSetEnvSecrets = vi.fn()
|
||||
const mockSetEnvironmentVariables = vi.fn()
|
||||
@ -283,7 +266,6 @@ describe('usePipelineInit', () => {
|
||||
mockFetchWorkflowDraft.mockRejectedValueOnce(mockJsonError)
|
||||
mockSyncWorkflowDraft.mockResolvedValue({ updated_at: '2024-01-02T00:00:00Z' })
|
||||
|
||||
// Second fetch succeeds
|
||||
mockFetchWorkflowDraft.mockResolvedValueOnce({
|
||||
graph: { nodes: [], edges: [], viewport: {} },
|
||||
hash: 'new-hash',
|
||||
@ -2,17 +2,8 @@ import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { act } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// ============================================================================
|
||||
// Import after mocks
|
||||
// ============================================================================
|
||||
import { usePipelineRefreshDraft } from '../use-pipeline-refresh-draft'
|
||||
|
||||
import { usePipelineRefreshDraft } from './use-pipeline-refresh-draft'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock workflow store
|
||||
const mockWorkflowStoreGetState = vi.fn()
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
useWorkflowStore: () => ({
|
||||
@ -20,7 +11,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useWorkflowUpdate
|
||||
const mockHandleUpdateWorkflowCanvas = vi.fn()
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowUpdate: () => ({
|
||||
@ -28,24 +18,18 @@ vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow service
|
||||
const mockFetchWorkflowDraft = vi.fn()
|
||||
vi.mock('@/service/workflow', () => ({
|
||||
fetchWorkflowDraft: (url: string) => mockFetchWorkflowDraft(url),
|
||||
}))
|
||||
|
||||
// Mock utils
|
||||
vi.mock('../utils', () => ({
|
||||
vi.mock('../../utils', () => ({
|
||||
processNodesWithoutDataSource: (nodes: unknown[], viewport: unknown) => ({
|
||||
nodes,
|
||||
viewport,
|
||||
}),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('usePipelineRefreshDraft', () => {
|
||||
const mockSetSyncWorkflowDraftHash = vi.fn()
|
||||
const mockSetIsSyncingWorkflowDraft = vi.fn()
|
||||
@ -4,17 +4,8 @@ import { act } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
// ============================================================================
|
||||
// Import after mocks
|
||||
// ============================================================================
|
||||
import { usePipelineRun } from '../use-pipeline-run'
|
||||
|
||||
import { usePipelineRun } from './use-pipeline-run'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock reactflow
|
||||
const mockStoreGetState = vi.fn()
|
||||
const mockGetViewport = vi.fn()
|
||||
vi.mock('reactflow', () => ({
|
||||
@ -26,7 +17,6 @@ vi.mock('reactflow', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow store
|
||||
const mockUseStore = vi.fn()
|
||||
const mockWorkflowStoreGetState = vi.fn()
|
||||
const mockWorkflowStoreSetState = vi.fn()
|
||||
@ -38,15 +28,13 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useNodesSyncDraft
|
||||
const mockDoSyncWorkflowDraft = vi.fn()
|
||||
vi.mock('./use-nodes-sync-draft', () => ({
|
||||
vi.mock('../use-nodes-sync-draft', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
doSyncWorkflowDraft: mockDoSyncWorkflowDraft,
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow hooks
|
||||
vi.mock('@/app/components/workflow/hooks/use-fetch-workflow-inspect-vars', () => ({
|
||||
useSetWorkflowVarsWithValue: () => ({
|
||||
fetchInspectVars: vi.fn(),
|
||||
@ -80,7 +68,6 @@ vi.mock('@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock service
|
||||
const mockSsePost = vi.fn()
|
||||
vi.mock('@/service/base', () => ({
|
||||
ssePost: (url: string, ...args: unknown[]) => mockSsePost(url, ...args),
|
||||
@ -96,17 +83,12 @@ vi.mock('@/service/use-workflow', () => ({
|
||||
useInvalidAllLastRun: () => mockInvalidAllLastRun,
|
||||
}))
|
||||
|
||||
// Mock FlowType
|
||||
vi.mock('@/types/common', () => ({
|
||||
FlowType: {
|
||||
ragPipeline: 'rag-pipeline',
|
||||
},
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('usePipelineRun', () => {
|
||||
const mockSetNodes = vi.fn()
|
||||
const mockGetNodes = vi.fn()
|
||||
@ -118,7 +100,6 @@ describe('usePipelineRun', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Mock DOM element
|
||||
const mockWorkflowContainer = document.createElement('div')
|
||||
mockWorkflowContainer.id = 'workflow-container'
|
||||
Object.defineProperty(mockWorkflowContainer, 'clientWidth', { value: 1000 })
|
||||
@ -466,7 +447,6 @@ describe('usePipelineRun', () => {
|
||||
await result.current.handleRun({ inputs: {} }, { onWorkflowStarted })
|
||||
})
|
||||
|
||||
// Trigger the callback
|
||||
await act(async () => {
|
||||
capturedCallbacks.onWorkflowStarted?.({ task_id: 'task-1' })
|
||||
})
|
||||
@ -743,7 +723,6 @@ describe('usePipelineRun', () => {
|
||||
capturedCallbacks.onTextChunk?.({ text: 'chunk' })
|
||||
})
|
||||
|
||||
// Just verify it doesn't throw
|
||||
expect(capturedCallbacks.onTextChunk).toBeDefined()
|
||||
})
|
||||
|
||||
@ -764,7 +743,6 @@ describe('usePipelineRun', () => {
|
||||
capturedCallbacks.onTextReplace?.({ text: 'replaced' })
|
||||
})
|
||||
|
||||
// Just verify it doesn't throw
|
||||
expect(capturedCallbacks.onTextReplace).toBeDefined()
|
||||
})
|
||||
|
||||
@ -794,12 +772,10 @@ describe('usePipelineRun', () => {
|
||||
|
||||
const { result } = renderHook(() => usePipelineRun())
|
||||
|
||||
// Run without any optional callbacks
|
||||
await act(async () => {
|
||||
await result.current.handleRun({ inputs: {} })
|
||||
})
|
||||
|
||||
// Trigger all callbacks - they should not throw even without optional handlers
|
||||
await act(async () => {
|
||||
capturedCallbacks.onWorkflowStarted?.({ task_id: 'task-1' })
|
||||
capturedCallbacks.onWorkflowFinished?.({ status: 'succeeded' })
|
||||
@ -818,7 +794,6 @@ describe('usePipelineRun', () => {
|
||||
capturedCallbacks.onTextReplace?.({ text: 'replaced' })
|
||||
})
|
||||
|
||||
// Verify ssePost was called
|
||||
expect(mockSsePost).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -3,17 +3,8 @@ import { act } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
|
||||
// ============================================================================
|
||||
// Import after mocks
|
||||
// ============================================================================
|
||||
import { usePipelineStartRun } from '../use-pipeline-start-run'
|
||||
|
||||
import { usePipelineStartRun } from './use-pipeline-start-run'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
|
||||
// Mock workflow store
|
||||
const mockWorkflowStoreGetState = vi.fn()
|
||||
const mockWorkflowStoreSetState = vi.fn()
|
||||
vi.mock('@/app/components/workflow/store', () => ({
|
||||
@ -23,7 +14,6 @@ vi.mock('@/app/components/workflow/store', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock workflow interactions
|
||||
const mockHandleCancelDebugAndPreviewPanel = vi.fn()
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useWorkflowInteractions: () => ({
|
||||
@ -31,7 +21,6 @@ vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Mock useNodesSyncDraft
|
||||
const mockDoSyncWorkflowDraft = vi.fn()
|
||||
vi.mock('@/app/components/rag-pipeline/hooks', () => ({
|
||||
useNodesSyncDraft: () => ({
|
||||
@ -42,10 +31,6 @@ vi.mock('@/app/components/rag-pipeline/hooks', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('usePipelineStartRun', () => {
|
||||
const mockSetIsPreparingDataSource = vi.fn()
|
||||
const mockSetShowEnvPanel = vi.fn()
|
||||
@ -210,7 +195,6 @@ describe('usePipelineStartRun', () => {
|
||||
result.current.handleStartWorkflowRun()
|
||||
})
|
||||
|
||||
// Should trigger the same workflow as handleWorkflowStartRunInWorkflow
|
||||
expect(mockSetShowEnvPanel).toHaveBeenCalledWith(false)
|
||||
})
|
||||
})
|
||||
@ -1,13 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { usePipelineTemplate } from './use-pipeline-template'
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
import { usePipelineTemplate } from '../use-pipeline-template'
|
||||
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
START_INITIAL_POSITION: { x: 100, y: 200 },
|
||||
@ -55,7 +49,6 @@ describe('usePipelineTemplate', () => {
|
||||
it('should position node offset from START_INITIAL_POSITION', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
// x = 100 + 500 = 600, y = 200
|
||||
expect(result.current.nodes[0].position.x).toBe(600)
|
||||
expect(result.current.nodes[0].position.y).toBe(200)
|
||||
})
|
||||
@ -63,6 +56,6 @@ describe('usePipelineTemplate', () => {
|
||||
it('should translate node title', () => {
|
||||
const { result } = renderHook(() => usePipelineTemplate())
|
||||
|
||||
expect(result.current.nodes[0].data.title).toBe('blocks.knowledge-base')
|
||||
expect(result.current.nodes[0].data.title).toBe('workflow.blocks.knowledge-base')
|
||||
})
|
||||
})
|
||||
@ -1,11 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { act } from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { usePipeline } from './use-pipeline'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
import { usePipeline } from '../use-pipeline'
|
||||
|
||||
const mockGetNodes = vi.fn()
|
||||
const mockSetNodes = vi.fn()
|
||||
@ -26,12 +22,12 @@ vi.mock('reactflow', () => ({
|
||||
|
||||
const mockFindUsedVarNodes = vi.fn()
|
||||
const mockUpdateNodeVars = vi.fn()
|
||||
vi.mock('../../workflow/nodes/_base/components/variable/utils', () => ({
|
||||
vi.mock('../../../workflow/nodes/_base/components/variable/utils', () => ({
|
||||
findUsedVarNodes: (...args: unknown[]) => mockFindUsedVarNodes(...args),
|
||||
updateNodeVars: (...args: unknown[]) => mockUpdateNodeVars(...args),
|
||||
}))
|
||||
|
||||
vi.mock('../../workflow/types', () => ({
|
||||
vi.mock('../../../workflow/types', () => ({
|
||||
BlockEnum: {
|
||||
DataSource: 'data-source',
|
||||
},
|
||||
@ -50,18 +46,10 @@ vi.mock('es-toolkit/compat', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Test data helpers
|
||||
// ============================================================================
|
||||
|
||||
function createNode(id: string, type: string) {
|
||||
return { id, data: { type }, position: { x: 0, y: 0 } }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('usePipeline', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -211,7 +199,6 @@ describe('usePipeline', () => {
|
||||
{ id: 'e1', source: 'ds-1', target: 'node-2' },
|
||||
{ id: 'e2', source: 'node-2', target: 'node-3' },
|
||||
)
|
||||
// Only node2 uses the variable
|
||||
mockFindUsedVarNodes.mockReturnValue([node2])
|
||||
const updatedNode2 = { ...node2, updated: true }
|
||||
mockUpdateNodeVars.mockReturnValue(updatedNode2)
|
||||
@ -227,7 +214,6 @@ describe('usePipeline', () => {
|
||||
})
|
||||
|
||||
const setNodesArg = mockSetNodes.mock.calls[0][0]
|
||||
// node2 should be updated, dsNode and node3 should remain the same
|
||||
expect(setNodesArg).toContain(dsNode)
|
||||
expect(setNodesArg).toContain(updatedNode2)
|
||||
expect(setNodesArg).toContain(node3)
|
||||
@ -288,11 +274,9 @@ describe('usePipeline', () => {
|
||||
|
||||
const { result } = renderHook(() => usePipeline())
|
||||
|
||||
// isVarUsedInNodes triggers getAllNodesInSameBranch internally
|
||||
const isUsed = result.current.isVarUsedInNodes(['rag', 'ds-1', 'var1'])
|
||||
expect(isUsed).toBe(true)
|
||||
|
||||
// Verify findUsedVarNodes was called with all downstream nodes (ds-1, node-2, node-3)
|
||||
const nodesArg = mockFindUsedVarNodes.mock.calls[0][1] as Array<{ id: string }>
|
||||
const nodeIds = nodesArg.map(n => n.id)
|
||||
expect(nodeIds).toContain('ds-1')
|
||||
@ -311,7 +295,6 @@ describe('usePipeline', () => {
|
||||
})
|
||||
|
||||
it('should deduplicate nodes when traversal finds shared nodes', () => {
|
||||
// Diamond graph: ds-1 -> n2, ds-1 -> n3, n2 -> n4, n3 -> n4
|
||||
const ds = createNode('ds-1', 'data-source')
|
||||
const n2 = createNode('node-2', 'llm')
|
||||
const n3 = createNode('node-3', 'llm')
|
||||
@ -329,7 +312,6 @@ describe('usePipeline', () => {
|
||||
|
||||
result.current.isVarUsedInNodes(['rag', 'ds-1', 'var1'])
|
||||
|
||||
// findUsedVarNodes should receive unique nodes (no duplicates of node-4)
|
||||
const nodesArg = mockFindUsedVarNodes.mock.calls[0][1] as Array<{ id: string }>
|
||||
const nodeIds = nodesArg.map(n => n.id)
|
||||
const uniqueIds = [...new Set(nodeIds)]
|
||||
@ -1,11 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useRagPipelineSearch } from './use-rag-pipeline-search'
|
||||
|
||||
// ============================================================================
|
||||
// Mocks
|
||||
// ============================================================================
|
||||
import { useRagPipelineSearch } from '../use-rag-pipeline-search'
|
||||
|
||||
const mockNodes: Array<{ id: string, data: Record<string, unknown> }> = []
|
||||
vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({
|
||||
@ -46,10 +42,6 @@ vi.mock('@/app/components/workflow/utils/node-navigation', () => ({
|
||||
setupNodeSelectionListener: () => mockCleanupListener,
|
||||
}))
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('useRagPipelineSearch', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@ -174,7 +166,6 @@ describe('useRagPipelineSearch', () => {
|
||||
const results = searchFn('')
|
||||
const titles = results.map(r => r.title)
|
||||
|
||||
// Should be alphabetically sorted
|
||||
const sortedTitles = [...titles].sort((a, b) => a.localeCompare(b))
|
||||
expect(titles).toEqual(sortedTitles)
|
||||
})
|
||||
@ -185,7 +176,6 @@ describe('useRagPipelineSearch', () => {
|
||||
const searchFn = mockRagPipelineNodesAction.searchFn!
|
||||
const results = searchFn('Search')
|
||||
|
||||
// "Web Search" has title match (score 10), "Knowledge Base" has desc match (score 5)
|
||||
expect(results[0].title).toBe('Web Search')
|
||||
})
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { DSLImportMode, DSLImportStatus } from '@/models/app'
|
||||
import { useUpdateDSLModal } from './use-update-dsl-modal'
|
||||
import { useUpdateDSLModal } from '../use-update-dsl-modal'
|
||||
|
||||
// --- FileReader stub ---
|
||||
class MockFileReader {
|
||||
onload: ((this: FileReader, event: ProgressEvent<FileReader>) => void) | null = null
|
||||
|
||||
@ -14,18 +13,12 @@ class MockFileReader {
|
||||
}
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader)
|
||||
|
||||
// --- Module-level mock functions ---
|
||||
const mockNotify = vi.fn()
|
||||
const mockEmit = vi.fn()
|
||||
const mockImportDSL = vi.fn()
|
||||
const mockImportDSLConfirm = vi.fn()
|
||||
const mockHandleCheckPluginDependencies = vi.fn()
|
||||
|
||||
// --- Mocks ---
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
vi.mock('use-context-selector', () => ({
|
||||
useContext: () => ({ notify: mockNotify }),
|
||||
}))
|
||||
@ -74,10 +67,8 @@ vi.mock('@/service/workflow', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// --- Helpers ---
|
||||
const createFile = () => new File(['test content'], 'test.pipeline', { type: 'text/yaml' })
|
||||
|
||||
// Cast MouseEventHandler to a plain callable for tests (event param is unused)
|
||||
type AsyncFn = () => Promise<void>
|
||||
|
||||
describe('useUpdateDSLModal', () => {
|
||||
@ -102,7 +93,6 @@ describe('useUpdateDSLModal', () => {
|
||||
mockHandleCheckPluginDependencies.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
// Initial state values
|
||||
describe('initial state', () => {
|
||||
it('should return correct defaults', () => {
|
||||
const { result } = renderUpdateDSLModal()
|
||||
@ -115,7 +105,6 @@ describe('useUpdateDSLModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// File handling
|
||||
describe('handleFile', () => {
|
||||
it('should set currentFile when file is provided', () => {
|
||||
const { result } = renderUpdateDSLModal()
|
||||
@ -142,7 +131,6 @@ describe('useUpdateDSLModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Modal state management
|
||||
describe('modal state', () => {
|
||||
it('should allow toggling showErrorModal', () => {
|
||||
const { result } = renderUpdateDSLModal()
|
||||
@ -161,7 +149,6 @@ describe('useUpdateDSLModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Import flow
|
||||
describe('handleImport', () => {
|
||||
it('should call importDSL with correct parameters', async () => {
|
||||
const { result } = renderUpdateDSLModal()
|
||||
@ -191,7 +178,6 @@ describe('useUpdateDSLModal', () => {
|
||||
expect(mockImportDSL).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// COMPLETED status
|
||||
it('should notify success on COMPLETED status', async () => {
|
||||
const { result } = renderUpdateDSLModal()
|
||||
act(() => {
|
||||
@ -257,7 +243,6 @@ describe('useUpdateDSLModal', () => {
|
||||
expect(mockHandleCheckPluginDependencies).toHaveBeenCalledWith('test-pipeline-id', true)
|
||||
})
|
||||
|
||||
// COMPLETED_WITH_WARNINGS status
|
||||
it('should notify warning on COMPLETED_WITH_WARNINGS status', async () => {
|
||||
mockImportDSL.mockResolvedValue({
|
||||
id: 'import-id',
|
||||
@ -277,7 +262,6 @@ describe('useUpdateDSLModal', () => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'warning' }))
|
||||
})
|
||||
|
||||
// PENDING status (version mismatch)
|
||||
it('should switch to version mismatch modal on PENDING status', async () => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true })
|
||||
|
||||
@ -338,7 +322,6 @@ describe('useUpdateDSLModal', () => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
// FAILED / unknown status
|
||||
it('should notify error on FAILED status', async () => {
|
||||
mockImportDSL.mockResolvedValue({
|
||||
id: 'import-id',
|
||||
@ -358,7 +341,6 @@ describe('useUpdateDSLModal', () => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
|
||||
})
|
||||
|
||||
// Exception
|
||||
it('should notify error when importDSL throws', async () => {
|
||||
mockImportDSL.mockRejectedValue(new Error('Network error'))
|
||||
|
||||
@ -374,7 +356,6 @@ describe('useUpdateDSLModal', () => {
|
||||
expect(mockNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
|
||||
})
|
||||
|
||||
// Missing pipeline_id
|
||||
it('should notify error when pipeline_id is missing on success', async () => {
|
||||
mockImportDSL.mockResolvedValue({
|
||||
id: 'import-id',
|
||||
@ -395,9 +376,7 @@ describe('useUpdateDSLModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Confirm flow (after PENDING → version mismatch)
|
||||
describe('onUpdateDSLConfirm', () => {
|
||||
// Helper: drive the hook into PENDING state so importId is set
|
||||
const setupPendingState = async (result: { current: ReturnType<typeof useUpdateDSLModal> }) => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true })
|
||||
|
||||
@ -520,7 +499,6 @@ describe('useUpdateDSLModal', () => {
|
||||
it('should not call importDSLConfirm when importId is not set', async () => {
|
||||
const { result } = renderUpdateDSLModal()
|
||||
|
||||
// No pending state → importId is undefined
|
||||
await act(async () => {
|
||||
await (result.current.onUpdateDSLConfirm as unknown as AsyncFn)()
|
||||
})
|
||||
@ -529,7 +507,6 @@ describe('useUpdateDSLModal', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Optional onImport callback
|
||||
describe('optional onImport', () => {
|
||||
it('should work without onImport callback', async () => {
|
||||
const { result } = renderHook(() =>
|
||||
@ -544,7 +521,6 @@ describe('useUpdateDSLModal', () => {
|
||||
await (result.current.handleImport as unknown as AsyncFn)()
|
||||
})
|
||||
|
||||
// Should succeed without throwing
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -1,13 +1,12 @@
|
||||
import type { InputFieldEditorProps } from '../components/panel/input-field/editor'
|
||||
import type { RagPipelineSliceShape } from './index'
|
||||
import type { InputFieldEditorProps } from '../../components/panel/input-field/editor'
|
||||
import type { RagPipelineSliceShape } from '../index'
|
||||
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
|
||||
import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PipelineInputVarType } from '@/models/pipeline'
|
||||
|
||||
import { createRagPipelineSliceSlice } from './index'
|
||||
import { createRagPipelineSliceSlice } from '../index'
|
||||
|
||||
// Mock the transformDataSourceToTool function
|
||||
vi.mock('@/app/components/workflow/block-selector/utils', () => ({
|
||||
transformDataSourceToTool: (item: DataSourceItem) => ({
|
||||
...item,
|
||||
@ -15,12 +14,10 @@ vi.mock('@/app/components/workflow/block-selector/utils', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
// Type-safe stubs for unused Zustand StateCreator params
|
||||
type SliceCreatorParams = Parameters<typeof createRagPipelineSliceSlice>
|
||||
const unusedGet = vi.fn() as unknown as SliceCreatorParams[1]
|
||||
const unusedApi = vi.fn() as unknown as SliceCreatorParams[2]
|
||||
|
||||
// Helper to create a slice with a given mockSet
|
||||
function createSlice(mockSet = vi.fn()) {
|
||||
return createRagPipelineSliceSlice(mockSet as unknown as SliceCreatorParams[0], unusedGet, unusedApi)
|
||||
}
|
||||
@ -92,7 +89,6 @@ describe('createRagPipelineSliceSlice', () => {
|
||||
|
||||
expect(mockSet).toHaveBeenCalledWith(expect.any(Function))
|
||||
|
||||
// Get the setter function and execute it
|
||||
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
|
||||
const result = setterFn()
|
||||
expect(result).toEqual({ showInputFieldPanel: true })
|
||||
@ -270,7 +266,6 @@ describe('RagPipelineSliceShape type', () => {
|
||||
it('should define all required properties', () => {
|
||||
const slice = createSlice()
|
||||
|
||||
// Check all properties exist
|
||||
expect(slice).toHaveProperty('pipelineId')
|
||||
expect(slice).toHaveProperty('knowledgeName')
|
||||
expect(slice).toHaveProperty('showInputFieldPanel')
|
||||
@ -2,9 +2,8 @@ import type { Viewport } from 'reactflow'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { processNodesWithoutDataSource } from './nodes'
|
||||
import { processNodesWithoutDataSource } from '../nodes'
|
||||
|
||||
// Mock constants
|
||||
vi.mock('@/app/components/workflow/constants', () => ({
|
||||
CUSTOM_NODE: 'custom',
|
||||
NODE_WIDTH_X_OFFSET: 400,
|
||||
@ -121,8 +120,6 @@ describe('processNodesWithoutDataSource', () => {
|
||||
|
||||
const result = processNodesWithoutDataSource(nodes, viewport)
|
||||
|
||||
// New nodes should be positioned based on the leftmost node (x: 200)
|
||||
// startX = 200 - 400 = -200
|
||||
expect(result.nodes[0].position.x).toBe(-200)
|
||||
expect(result.nodes[0].position.y).toBe(100)
|
||||
})
|
||||
@ -140,10 +137,6 @@ describe('processNodesWithoutDataSource', () => {
|
||||
|
||||
const result = processNodesWithoutDataSource(nodes, viewport)
|
||||
|
||||
// startX = 300 - 400 = -100
|
||||
// startY = 200
|
||||
// viewport.x = (100 - (-100)) * 1 = 200
|
||||
// viewport.y = (100 - 200) * 1 = -100
|
||||
expect(result.viewport).toEqual({
|
||||
x: 200,
|
||||
y: -100,
|
||||
@ -164,10 +157,6 @@ describe('processNodesWithoutDataSource', () => {
|
||||
|
||||
const result = processNodesWithoutDataSource(nodes, viewport)
|
||||
|
||||
// startX = 300 - 400 = -100
|
||||
// startY = 200
|
||||
// viewport.x = (100 - (-100)) * 2 = 400
|
||||
// viewport.y = (100 - 200) * 2 = -200
|
||||
expect(result.viewport).toEqual({
|
||||
x: 400,
|
||||
y: -200,
|
||||
@ -202,7 +191,6 @@ describe('processNodesWithoutDataSource', () => {
|
||||
|
||||
const result = processNodesWithoutDataSource(nodes)
|
||||
|
||||
// Data source empty node position
|
||||
const dataSourceEmptyNode = result.nodes[0]
|
||||
const noteNode = result.nodes[1]
|
||||
|
||||
@ -276,7 +264,6 @@ describe('processNodesWithoutDataSource', () => {
|
||||
|
||||
const result = processNodesWithoutDataSource(nodes, viewport)
|
||||
|
||||
// No custom nodes to find leftmost, so no new nodes are added
|
||||
expect(result.nodes).toBe(nodes)
|
||||
expect(result.viewport).toBe(viewport)
|
||||
})
|
||||
@ -301,7 +288,6 @@ describe('processNodesWithoutDataSource', () => {
|
||||
|
||||
const result = processNodesWithoutDataSource(nodes)
|
||||
|
||||
// First node should be used as leftNode
|
||||
expect(result.nodes.length).toBe(4)
|
||||
})
|
||||
|
||||
@ -317,7 +303,6 @@ describe('processNodesWithoutDataSource', () => {
|
||||
|
||||
const result = processNodesWithoutDataSource(nodes)
|
||||
|
||||
// startX = -100 - 400 = -500
|
||||
expect(result.nodes[0].position.x).toBe(-500)
|
||||
expect(result.nodes[0].position.y).toBe(-50)
|
||||
})
|
||||
Reference in New Issue
Block a user