From 7058f90fd1ee1ecfd98b93cfc46e699a29be5f2d Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Wed, 11 Feb 2026 15:38:42 +0800 Subject: [PATCH] test: add comprehensive unit tests for RagPipeline components including conversion, publish modal, and toast notifications --- .../{ => __tests__}/index.spec.tsx | 52 +- .../{ => __tests__}/conversion.spec.tsx | 35 +- .../components/{ => __tests__}/index.spec.tsx | 185 +----- ...blish-as-knowledge-pipeline-modal.spec.tsx | 23 +- .../{ => __tests__}/publish-toast.spec.tsx | 32 +- .../rag-pipeline-main.spec.tsx | 18 +- .../{ => __tests__}/update-dsl-modal.spec.tsx | 203 +++---- .../version-mismatch-modal.spec.tsx | 2 +- .../{ => __tests__}/chunk-card.spec.tsx | 14 +- .../{ => __tests__}/index.spec.tsx | 201 +------ .../panel/{ => __tests__}/index.spec.tsx | 218 +------ .../{ => __tests__}/footer-tip.spec.tsx | 3 +- .../input-field/{ => __tests__}/hooks.spec.ts | 14 +- .../{ => __tests__}/index.spec.tsx | 245 +------- .../editor/{ => __tests__}/index.spec.tsx | 298 +--------- .../editor/form/{ => __tests__}/hooks.spec.ts | 49 +- .../form/{ => __tests__}/index.spec.tsx | 357 +----------- .../form/{ => __tests__}/schema.spec.ts | 11 +- .../field-list/{ => __tests__}/hooks.spec.ts | 30 +- .../field-list/{ => __tests__}/index.spec.tsx | 433 +------------- .../{ => __tests__}/index.spec.tsx | 23 +- .../preview/{ => __tests__}/index.spec.tsx | 317 +---------- .../test-run/{ => __tests__}/index.spec.tsx | 118 +--- .../preparation/{ => __tests__}/hooks.spec.ts | 20 +- .../{ => __tests__}/index.spec.tsx | 532 +----------------- .../actions/{ => __tests__}/index.spec.tsx | 143 +---- .../{ => __tests__}/index.spec.tsx | 315 +---------- .../{ => __tests__}/index.spec.tsx | 280 +-------- .../result/{ => __tests__}/index.spec.tsx | 159 +----- .../{ => __tests__}/index.spec.tsx | 287 +--------- .../tabs/{ => __tests__}/index.spec.tsx | 232 +------- .../{ => __tests__}/index.spec.tsx | 131 +---- .../{ => __tests__}/run-mode.spec.tsx | 21 +- .../publisher/{ => __tests__}/index.spec.tsx | 278 +-------- .../publisher/{ => __tests__}/popup.spec.tsx | 37 +- .../hooks/{ => __tests__}/index.spec.ts | 37 +- .../hooks/{ => __tests__}/use-DSL.spec.ts | 25 +- .../use-available-nodes-meta-data.spec.ts | 12 +- .../{ => __tests__}/use-configs-map.spec.ts | 2 +- .../use-get-run-and-trace-url.spec.ts | 2 +- .../use-input-field-panel.spec.ts | 2 +- .../{ => __tests__}/use-input-fields.spec.ts | 2 +- .../use-nodes-sync-draft.spec.ts | 26 +- .../use-pipeline-config.spec.ts | 20 +- .../{ => __tests__}/use-pipeline-init.spec.ts | 24 +- .../use-pipeline-refresh-draft.spec.ts | 20 +- .../{ => __tests__}/use-pipeline-run.spec.ts | 29 +- .../use-pipeline-start-run.spec.ts | 18 +- .../use-pipeline-template.spec.ts | 11 +- .../{ => __tests__}/use-pipeline.spec.ts | 24 +- .../use-rag-pipeline-search.spec.tsx | 12 +- .../use-update-dsl-modal.spec.ts | 26 +- .../store/{ => __tests__}/index.spec.ts | 11 +- .../utils/{ => __tests__}/index.spec.ts | 17 +- 54 files changed, 311 insertions(+), 5325 deletions(-) rename web/app/components/rag-pipeline/{ => __tests__}/index.spec.tsx (87%) rename web/app/components/rag-pipeline/components/{ => __tests__}/conversion.spec.tsx (80%) rename web/app/components/rag-pipeline/components/{ => __tests__}/index.spec.tsx (82%) rename web/app/components/rag-pipeline/components/{ => __tests__}/publish-as-knowledge-pipeline-modal.spec.tsx (92%) rename web/app/components/rag-pipeline/components/{ => __tests__}/publish-toast.spec.tsx (69%) rename web/app/components/rag-pipeline/components/{ => __tests__}/rag-pipeline-main.spec.tsx (94%) rename web/app/components/rag-pipeline/components/{ => __tests__}/update-dsl-modal.spec.tsx (77%) rename web/app/components/rag-pipeline/components/{ => __tests__}/version-mismatch-modal.spec.tsx (98%) rename web/app/components/rag-pipeline/components/chunk-card-list/{ => __tests__}/chunk-card.spec.tsx (94%) rename web/app/components/rag-pipeline/components/chunk-card-list/{ => __tests__}/index.spec.tsx (83%) rename web/app/components/rag-pipeline/components/panel/{ => __tests__}/index.spec.tsx (76%) rename web/app/components/rag-pipeline/components/panel/input-field/{ => __tests__}/footer-tip.spec.tsx (94%) rename web/app/components/rag-pipeline/components/panel/input-field/{ => __tests__}/hooks.spec.ts (87%) rename web/app/components/rag-pipeline/components/panel/input-field/{ => __tests__}/index.spec.tsx (78%) rename web/app/components/rag-pipeline/components/panel/input-field/editor/{ => __tests__}/index.spec.tsx (82%) rename web/app/components/rag-pipeline/components/panel/input-field/editor/form/{ => __tests__}/hooks.spec.ts (88%) rename web/app/components/rag-pipeline/components/panel/input-field/editor/form/{ => __tests__}/index.spec.tsx (77%) rename web/app/components/rag-pipeline/components/panel/input-field/editor/form/{ => __tests__}/schema.spec.ts (94%) rename web/app/components/rag-pipeline/components/panel/input-field/field-list/{ => __tests__}/hooks.spec.ts (91%) rename web/app/components/rag-pipeline/components/panel/input-field/field-list/{ => __tests__}/index.spec.tsx (80%) rename web/app/components/rag-pipeline/components/panel/input-field/label-right-content/{ => __tests__}/index.spec.tsx (90%) rename web/app/components/rag-pipeline/components/panel/input-field/preview/{ => __tests__}/index.spec.tsx (75%) rename web/app/components/rag-pipeline/components/panel/test-run/{ => __tests__}/index.spec.tsx (85%) rename web/app/components/rag-pipeline/components/panel/test-run/preparation/{ => __tests__}/hooks.spec.ts (92%) rename web/app/components/rag-pipeline/components/panel/test-run/preparation/{ => __tests__}/index.spec.tsx (76%) rename web/app/components/rag-pipeline/components/panel/test-run/preparation/actions/{ => __tests__}/index.spec.tsx (74%) rename web/app/components/rag-pipeline/components/panel/test-run/preparation/data-source-options/{ => __tests__}/index.spec.tsx (80%) rename web/app/components/rag-pipeline/components/panel/test-run/preparation/document-processing/{ => __tests__}/index.spec.tsx (85%) rename web/app/components/rag-pipeline/components/panel/test-run/result/{ => __tests__}/index.spec.tsx (81%) rename web/app/components/rag-pipeline/components/panel/test-run/result/result-preview/{ => __tests__}/index.spec.tsx (79%) rename web/app/components/rag-pipeline/components/panel/test-run/result/tabs/{ => __tests__}/index.spec.tsx (81%) rename web/app/components/rag-pipeline/components/rag-pipeline-header/{ => __tests__}/index.spec.tsx (84%) rename web/app/components/rag-pipeline/components/rag-pipeline-header/{ => __tests__}/run-mode.spec.tsx (90%) rename web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/{ => __tests__}/index.spec.tsx (81%) rename web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/{ => __tests__}/popup.spec.tsx (87%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/index.spec.ts (92%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-DSL.spec.ts (90%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-available-nodes-meta-data.spec.ts (93%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-configs-map.spec.ts (97%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-get-run-and-trace-url.spec.ts (95%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-input-field-panel.spec.ts (98%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-input-fields.spec.ts (99%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-nodes-sync-draft.spec.ts (93%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-pipeline-config.spec.ts (92%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-pipeline-init.spec.ts (92%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-pipeline-refresh-draft.spec.ts (90%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-pipeline-run.spec.ts (96%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-pipeline-start-run.spec.ts (90%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-pipeline-template.spec.ts (87%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-pipeline.spec.ts (90%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-rag-pipeline-search.spec.tsx (92%) rename web/app/components/rag-pipeline/hooks/{ => __tests__}/use-update-dsl-modal.spec.ts (94%) rename web/app/components/rag-pipeline/store/{ => __tests__}/index.spec.ts (95%) rename web/app/components/rag-pipeline/utils/{ => __tests__}/index.spec.ts (93%) diff --git a/web/app/components/rag-pipeline/index.spec.tsx b/web/app/components/rag-pipeline/__tests__/index.spec.tsx similarity index 87% rename from web/app/components/rag-pipeline/index.spec.tsx rename to web/app/components/rag-pipeline/__tests__/index.spec.tsx index ed7ef0c367..221713defe 100644 --- a/web/app/components/rag-pipeline/index.spec.tsx +++ b/web/app/components/rag-pipeline/__tests__/index.spec.tsx @@ -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: () =>
Conversion Component
, })) -// 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 } }) => (
{nodes?.length ?? 0} @@ -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 }) => (
{children}
), })) -// Mock: Context provider vi.mock('@/app/components/workflow/context', () => ({ WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => (
{children}
), })) -// 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) => 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 => ({ 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() - // Real Loading component has role="status" expect(screen.getByRole('status')).toBeInTheDocument() }) @@ -240,8 +223,6 @@ describe('RagPipeline', () => { render() - // 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() - // 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() - // When data is undefined, Loading is shown, processNodesWithoutDataSource is not called expect(mockProcessNodesWithoutDataSource).not.toHaveBeenCalled() }) @@ -279,13 +258,10 @@ describe('RagPipeline', () => { const { rerender } = render() - // Clear mock call count after initial render mockProcessNodesWithoutDataSource.mockClear() - // Rerender with same data reference (no change to mockUsePipelineInit) rerender() - // 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() - // 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() @@ -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() 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()).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()).toThrow() consoleSpy.mockRestore() diff --git a/web/app/components/rag-pipeline/components/conversion.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx similarity index 80% rename from web/app/components/rag-pipeline/components/conversion.spec.tsx rename to web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx index 8dac542428..2bd20fb5c3 100644 --- a/web/app/components/rag-pipeline/components/conversion.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/conversion.spec.tsx @@ -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: () =>
, })) @@ -82,21 +75,21 @@ describe('Conversion', () => { it('should render conversion title and description', () => { render() - 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() - expect(screen.getByText('operations.convert')).toBeInTheDocument() + expect(screen.getByText('datasetPipeline.operations.convert')).toBeInTheDocument() }) it('should render warning text', () => { render() - 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() - 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() - 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() - 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() - 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() - 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({ diff --git a/web/app/components/rag-pipeline/components/index.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx similarity index 82% rename from web/app/components/rag-pipeline/components/index.spec.tsx rename to web/app/components/rag-pipeline/components/__tests__/index.spec.tsx index e17f07303d..5c3781e8c1 100644 --- a/web/app/components/rag-pipeline/components/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/index.spec.tsx @@ -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 }>, 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: (selector: (state: ReturnType) => T): T => selector(createMockProviderContextValue()), })) -// Mock WorkflowWithInnerContext vi.mock('@/app/components/workflow', () => ({ WorkflowWithInnerContext: ({ children }: PropsWithChildren) => (
{children}
), })) -// Mock workflow panel vi.mock('@/app/components/workflow/panel', () => ({ default: ({ components }: { components?: { left?: React.ReactNode, right?: React.ReactNode } }) => (
@@ -316,19 +290,16 @@ vi.mock('@/app/components/workflow/panel', () => ({ ), })) -// Mock PluginDependency -vi.mock('../../workflow/plugin-dependency', () => ({ +vi.mock('../../../workflow/plugin-dependency', () => ({ default: () =>
, })) -// 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 }) => (
@@ -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: () =>
, })) -// Mock PublishToast -vi.mock('./publish-toast', () => ({ +vi.mock('../publish-toast', () => ({ default: () =>
, })) -// 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() @@ -600,9 +550,6 @@ describe('Conversion', () => { }) }) - // -------------------------------------------------------------------------- - // User Interactions Tests - // -------------------------------------------------------------------------- describe('User Interactions', () => { it('should show confirm modal when convert button is clicked', () => { render() @@ -617,20 +564,15 @@ describe('Conversion', () => { it('should hide confirm modal when cancel is clicked', () => { render() - // 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() - // 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() - // Rerender should not cause issues with callback rerender() expect(screen.getByRole('button', { name: /datasetPipeline\.operations\.convert/i })).toBeInTheDocument() }) }) - // -------------------------------------------------------------------------- - // Edge Cases Tests - // -------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle missing datasetId gracefully', () => { render() - // 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() @@ -770,14 +697,10 @@ describe('PipelineScreenShot', () => { render() 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() @@ -863,9 +776,6 @@ describe('PublishAsKnowledgePipelineModal', () => { }) }) - // -------------------------------------------------------------------------- - // User Interactions Tests - // -------------------------------------------------------------------------- describe('User Interactions', () => { it('should update name when input changes', () => { render() @@ -906,11 +816,9 @@ describe('PublishAsKnowledgePipelineModal', () => { render() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // Rerender should not cause issues rerender() 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() @@ -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() @@ -1090,9 +971,6 @@ describe('RagPipelineChildren', () => { }) }) - // -------------------------------------------------------------------------- - // Event Subscription Tests - covers lines 37-40 - // -------------------------------------------------------------------------- describe('Event Subscription', () => { it('should subscribe to event emitter', () => { render() @@ -1103,12 +981,10 @@ describe('RagPipelineChildren', () => { it('should handle DSL_EXPORT_CHECK event and set secretEnvList', async () => { render() - // 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() - // 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() - // 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() - // 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() - // 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', () => { diff --git a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx similarity index 92% rename from web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.spec.tsx rename to web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx index 51b0e1d1b2..0d6687cbed 100644 --- a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx @@ -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() 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() - 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() - 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() - 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() - 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', diff --git a/web/app/components/rag-pipeline/components/publish-toast.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/publish-toast.spec.tsx similarity index 69% rename from web/app/components/rag-pipeline/components/publish-toast.spec.tsx rename to web/app/components/rag-pipeline/components/__tests__/publish-toast.spec.tsx index d61f091ed2..0e65f8f1db 100644 --- a/web/app/components/rag-pipeline/components/publish-toast.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/publish-toast.spec.tsx @@ -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) => unknown) => { @@ -32,19 +24,19 @@ describe('PublishToast', () => { mockPublishedAt = 0 render() - expect(screen.getByText('publishToast.title')).toBeInTheDocument() + expect(screen.getByText('pipeline.publishToast.title')).toBeInTheDocument() }) it('should render toast title', () => { render() - expect(screen.getByText('publishToast.title')).toBeInTheDocument() + expect(screen.getByText('pipeline.publishToast.title')).toBeInTheDocument() }) it('should render toast description', () => { render() - 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() - 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() - // 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() - // 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() - // 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() - // The close button is a div with cursor-pointer, not a semantic button const closeButton = container.querySelector('.cursor-pointer') fireEvent.click(closeButton!) rerender() - 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() - 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() - const toastContainer = screen.getByText('publishToast.title').closest('.rounded-xl') + const toastContainer = screen.getByText('pipeline.publishToast.title').closest('.rounded-xl') expect(toastContainer).toBeInTheDocument() }) }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-main.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/rag-pipeline-main.spec.tsx similarity index 94% rename from web/app/components/rag-pipeline/components/rag-pipeline-main.spec.tsx rename to web/app/components/rag-pipeline/components/__tests__/rag-pipeline-main.spec.tsx index 3de3c3deeb..22d38861da 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-main.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/rag-pipeline-main.spec.tsx @@ -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 }>) => (
@@ -108,8 +102,7 @@ vi.mock('@/app/components/workflow', () => ({ ), })) -// Mock RagPipelineChildren -vi.mock('./rag-pipeline-children', () => ({ +vi.mock('../rag-pipeline-children', () => ({ default: () =>
Children
, })) @@ -201,7 +194,6 @@ describe('RagPipelineMain', () => { it('should use useNodesSyncDraft hook', () => { render() - // If the component renders, the hook was called successfully expect(screen.getByTestId('workflow-inner-context')).toBeInTheDocument() }) diff --git a/web/app/components/rag-pipeline/components/update-dsl-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx similarity index 77% rename from web/app/components/rag-pipeline/components/update-dsl-modal.spec.tsx rename to web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx index addfa3dc53..2f9b2172bd 100644 --- a/web/app/components/rag-pipeline/components/update-dsl-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx @@ -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) => 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 }) => (
@@ -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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - 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() - 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() - // 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() - 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() - // 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() - 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() - // 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() - // 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() - // 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(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(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(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(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() - // 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() }) }) diff --git a/web/app/components/rag-pipeline/components/version-mismatch-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/version-mismatch-modal.spec.tsx similarity index 98% rename from web/app/components/rag-pipeline/components/version-mismatch-modal.spec.tsx rename to web/app/components/rag-pipeline/components/__tests__/version-mismatch-modal.spec.tsx index b14cdcf9c1..087f900f8a 100644 --- a/web/app/components/rag-pipeline/components/version-mismatch-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/version-mismatch-modal.spec.tsx @@ -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() diff --git a/web/app/components/rag-pipeline/components/chunk-card-list/chunk-card.spec.tsx b/web/app/components/rag-pipeline/components/chunk-card-list/__tests__/chunk-card.spec.tsx similarity index 94% rename from web/app/components/rag-pipeline/components/chunk-card-list/chunk-card.spec.tsx rename to web/app/components/rag-pipeline/components/chunk-card-list/__tests__/chunk-card.spec.tsx index 5db52237dd..59cd9613f3 100644 --- a/web/app/components/rag-pipeline/components/chunk-card-list/chunk-card.spec.tsx +++ b/web/app/components/rag-pipeline/components/chunk-card-list/__tests__/chunk-card.spec.tsx @@ -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) => `${key}${opts?.count !== undefined ? `:${opts.count}` : ''}`, - }), -})) +import ChunkCard from '../chunk-card' vi.mock('@/app/components/datasets/documents/detail/completed/common/dot', () => ({ default: () => , @@ -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 }) => ( {text} ), })) -vi.mock('./types', () => ({ +vi.mock('../types', () => ({ QAItemType: { Question: 'question', Answer: 'answer', diff --git a/web/app/components/rag-pipeline/components/chunk-card-list/index.spec.tsx b/web/app/components/rag-pipeline/components/chunk-card-list/__tests__/index.spec.tsx similarity index 83% rename from web/app/components/rag-pipeline/components/chunk-card-list/index.spec.tsx rename to web/app/components/rag-pipeline/components/chunk-card-list/__tests__/index.spec.tsx index ca5fae25c7..2fab56f0ea 100644 --- a/web/app/components/rag-pipeline/components/chunk-card-list/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/chunk-card-list/__tests__/index.spec.tsx @@ -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 => ({ ...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() - // Assert expect(screen.getByText('Q')).toBeInTheDocument() expect(screen.getByText('What is this?')).toBeInTheDocument() }) it('should render answer type with A prefix', () => { - // Arrange & Act render() - // 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() - // Assert expect(screen.getByText('Q')).toBeInTheDocument() }) it('should render with long text content', () => { - // Arrange const longText = 'A'.repeat(1000) - // Act render() - // Assert expect(screen.getByText(longText)).toBeInTheDocument() }) it('should render with special characters in text', () => { - // Arrange const specialText = ' & "quotes" \'apostrophe\'' - // Act render() - // Assert expect(screen.getByText(specialText)).toBeInTheDocument() }) }) - // Tests for memoization behavior describe('Memoization', () => { it('should be memoized with React.memo', () => { - // Arrange & Act const { rerender } = render() - // Assert - component should render consistently expect(screen.getByText('Q')).toBeInTheDocument() expect(screen.getByText('Test')).toBeInTheDocument() - // Rerender with same props - should not cause issues rerender() 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // Assert expect(screen.getByText(/Parent-Chunk-01/)).toBeInTheDocument() }) it('should hide segment index tag for full-doc mode', () => { - // Arrange & Act render( { />, ) - // 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( { />, ) - // 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( { />, ) - // Assert - formatNumber(1234) returns '1,234' expect(screen.getByText(/1,234/)).toBeInTheDocument() }) it('should display word count with character translation key', () => { - // Arrange & Act render( { />, ) - // 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( { />, ) - // 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( { />, ) - // Assert expect(screen.getByText(/Chunk-42/)).toBeInTheDocument() }) it('should handle string position ID', () => { - // Arrange & Act render( { />, ) - // Assert expect(screen.getByText(/Chunk-99/)).toBeInTheDocument() }) it('should pad single digit position ID', () => { - // Arrange & Act render( { />, ) - // 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( { />, ) - // Assert - paragraph mode shows label expect(screen.getByText(/Parent-Chunk/)).toBeInTheDocument() - // Act - change to full-doc rerender( { />, ) - // 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( { />, ) - // 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( { />, ) - // Assert expect(screen.getByText('Text content')).toBeInTheDocument() - // Act - change to QA type const qaContent: QAChunk = { question: 'Q?', answer: 'A.' } rerender( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // Assert expect(screen.getByText(longContent)).toBeInTheDocument() }) it('should handle zero word count', () => { - // Arrange & Act render( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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( { />, ) - // 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) }) }) diff --git a/web/app/components/rag-pipeline/components/panel/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/__tests__/index.spec.tsx similarity index 76% rename from web/app/components/rag-pipeline/components/panel/index.spec.tsx rename to web/app/components/rag-pipeline/components/panel/__tests__/index.spec.tsx index 11f9f8b2c4..f651b16697 100644 --- a/web/app/components/rag-pipeline/components/panel/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/__tests__/index.spec.tsx @@ -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 () =>
Record Panel
@@ -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) => { return dynamicMocks.createMockComponent() }, })) -// Mock workflow store let mockHistoryWorkflowData: Record | 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 | 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() - // Assert await waitFor(() => { expect(screen.getByTestId('workflow-panel')).toBeInTheDocument() }) }) it('should render Panel component with correct structure', async () => { - // Act render() - // 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() - // 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() - // 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() - // 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() - // 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() - // Assert await waitFor(() => { expect(capturedPanelProps?.versionHistoryPanelProps?.latestVersionId).toBe('') }) }) }) - // ------------------------------------------------------------------------- - // Memoization Tests - panelProps - // ------------------------------------------------------------------------- describe('Memoization - panelProps', () => { it('should pass components.left to Panel', async () => { - // Act render() - // Assert await waitFor(() => { expect(capturedPanelProps?.components?.left).toBeDefined() }) }) it('should pass components.right to Panel', async () => { - // Act render() - // Assert await waitFor(() => { expect(capturedPanelProps?.components?.right).toBeDefined() }) }) it('should pass versionHistoryPanelProps to panelProps', async () => { - // Act render() - // 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() - // Act - rerender without prop changes rerender() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // Act - Multiple rerenders for (let i = 0; i < 10; i++) rerender() - // 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() - // 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() - // Assert - All store-dependent rendering should work await waitFor(() => { expect(screen.getByTestId('record-panel')).toBeInTheDocument() expect(screen.getByTestId('test-run-panel')).toBeInTheDocument() diff --git a/web/app/components/rag-pipeline/components/panel/input-field/footer-tip.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/footer-tip.spec.tsx similarity index 94% rename from web/app/components/rag-pipeline/components/panel/input-field/footer-tip.spec.tsx rename to web/app/components/rag-pipeline/components/panel/input-field/__tests__/footer-tip.spec.tsx index 5d5cde9735..f70b9a4a6f 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/footer-tip.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/footer-tip.spec.tsx @@ -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() - // The RiDragDropLine icon should be rendered const icon = container.querySelector('.size-4') expect(icon).toBeInTheDocument() }) diff --git a/web/app/components/rag-pipeline/components/panel/input-field/hooks.spec.ts b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/hooks.spec.ts similarity index 87% rename from web/app/components/rag-pipeline/components/panel/input-field/hooks.spec.ts rename to web/app/components/rag-pipeline/components/panel/input-field/__tests__/hooks.spec.ts index 452963ba7f..9f7fb7e902 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/hooks.spec.ts +++ b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/hooks.spec.ts @@ -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() }) }) diff --git a/web/app/components/rag-pipeline/components/panel/input-field/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/index.spec.tsx similarity index 78% rename from web/app/components/rag-pipeline/components/panel/input-field/index.spec.tsx rename to web/app/components/rag-pipeline/components/panel/input-field/__tests__/index.spec.tsx index 0ff7a06dae..ab99a1eeef 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/__tests__/index.spec.tsx @@ -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[] = [] 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: () =>
Footer Tip
, })) -// Mock Datasource label component -vi.mock('./label-right-content/datasource', () => ({ +vi.mock('../label-right-content/datasource', () => ({ default: ({ nodeData }: { nodeData: DataSourceNodeType }) => (
{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: () =>
Global Inputs
, })) -// ============================================================================ -// Test Data Factories -// ============================================================================ - const createInputVar = (overrides?: Partial): InputVar => ({ type: PipelineInputVarType.textInput, label: 'Test Label', @@ -189,10 +173,6 @@ const createDataSourceNode = ( } as DataSourceNodeType, }) -// ============================================================================ -// Helper Functions -// ============================================================================ - const setupMocks = (options?: { nodes?: Node[] 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() - // Assert expect( screen.getByText('datasetPipeline.inputFieldPanel.title'), ).toBeInTheDocument() }) it('should render panel title correctly', () => { - // Act render() - // Assert expect( screen.getByText('datasetPipeline.inputFieldPanel.title'), ).toBeInTheDocument() }) it('should render panel description', () => { - // Act render() - // Assert expect( screen.getByText('datasetPipeline.inputFieldPanel.description'), ).toBeInTheDocument() }) it('should render preview button', () => { - // Act render() - // Assert expect( screen.getByText('datasetPipeline.operations.preview'), ).toBeInTheDocument() }) it('should render close button', () => { - // Act render() - // Assert const closeButton = screen.getByRole('button', { name: '' }) expect(closeButton).toBeInTheDocument() }) it('should render footer tip component', () => { - // Act render() - // Assert expect(screen.getByTestId('footer-tip')).toBeInTheDocument() }) it('should render unique inputs section title', () => { - // Act render() - // Assert expect( screen.getByText('datasetPipeline.inputFieldPanel.uniqueInputs.title'), ).toBeInTheDocument() }) it('should render global inputs field list', () => { - // Act render() - // 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() - // 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() - // 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() - // 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 mockNodesData = [dataSourceNode, otherNode] - // Act render() - // 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() - // 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() - // 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() - // 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() 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() 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() - // 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() - // 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() - // 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() - // Assert expect(screen.getByTestId('field-list-readonly-shared')).toHaveTextContent( 'true', ) }) it('should set readonly to true when editing', () => { - // Arrange setupMocks({ isEditing: true }) - // Act render() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // Act - rerender with same nodes reference rerender() - // 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() - // 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() - // 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() - // Act fireEvent.click(screen.getByText('datasetPipeline.operations.preview')) const callCount1 = mockToggleInputFieldPreviewPanel.mock.calls.length rerender() 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // 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() - // Assert const panel = container.firstChild as HTMLElement expect(panel).toHaveClass('w-[400px]') }) it('should have overflow scroll on content area', () => { - // Act const { container } = render() - // Assert const scrollContainer = container.querySelector('.overflow-y-auto') expect(scrollContainer).toBeInTheDocument() }) it('should render header section with proper spacing', () => { - // Act render() - // 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() - // 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() - // 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() - // 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() 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() - // 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() - // 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() - // 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() - // Act - rerender without prop changes rerender() - // 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() - // Act - trigger a state change fireEvent.click(screen.getByTestId('trigger-change-node-1')) - // Assert await waitFor(() => { expect(mockSetRagPipelineVariables).toHaveBeenCalled() }) diff --git a/web/app/components/rag-pipeline/components/panel/input-field/editor/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/input-field/editor/__tests__/index.spec.tsx similarity index 82% rename from web/app/components/rag-pipeline/components/panel/input-field/editor/index.spec.tsx rename to web/app/components/rag-pipeline/components/panel/input-field/editor/__tests__/index.spec.tsx index 4e7f4f504d..d8feea44c6 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/editor/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/editor/__tests__/index.spec.tsx @@ -1,5 +1,5 @@ -import type { FormData } from './form/types' -import type { InputFieldEditorProps } from './index' +import type { FormData } from '../form/types' +import type { InputFieldEditorProps } from '../index' import type { SupportUploadFileTypes } from '@/app/components/workflow/types' import type { InputVar } from '@/models/pipeline' import type { TransferMethod } from '@/types/app' @@ -7,28 +7,22 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' import { PipelineInputVarType } from '@/models/pipeline' -import InputFieldEditorPanel from './index' +import InputFieldEditorPanel from '../index' import { convertFormDataToINputField, convertToInputFieldFormData, -} from './utils' +} from '../utils' -// ============================================================================ -// Mock External Dependencies -// ============================================================================ - -// Mock useFloatingRight hook const mockUseFloatingRight = vi.fn(() => ({ floatingRight: false, floatingRightWidth: 400, })) -vi.mock('../hooks', () => ({ +vi.mock('../../hooks', () => ({ useFloatingRight: () => mockUseFloatingRight(), })) -// Mock InputFieldForm component -vi.mock('./form', () => ({ +vi.mock('../form', () => ({ default: ({ initialData, supportFile, @@ -57,7 +51,6 @@ vi.mock('./form', () => ({ ), })) -// Mock file upload config service vi.mock('@/service/use-common', () => ({ useFileUploadConfig: () => ({ data: { @@ -72,10 +65,6 @@ vi.mock('@/service/use-common', () => ({ }), })) -// ============================================================================ -// Test Data Factories -// ============================================================================ - const createInputVar = (overrides?: Partial): InputVar => ({ type: PipelineInputVarType.textInput, label: 'Test Label', @@ -120,10 +109,6 @@ const createInputFieldEditorProps = ( ...overrides, }) -// ============================================================================ -// Test Wrapper Component -// ============================================================================ - const createTestQueryClient = () => new QueryClient({ defaultOptions: { @@ -145,10 +130,6 @@ const renderWithProviders = (ui: React.ReactElement) => { return render(ui, { wrapper: TestWrapper }) } -// ============================================================================ -// InputFieldEditorPanel Component Tests -// ============================================================================ - describe('InputFieldEditorPanel', () => { beforeEach(() => { vi.clearAllMocks() @@ -158,103 +139,75 @@ describe('InputFieldEditorPanel', () => { }) }) - // ------------------------------------------------------------------------- - // Rendering Tests - // ------------------------------------------------------------------------- describe('Rendering', () => { it('should render panel without crashing', () => { - // Arrange const props = createInputFieldEditorProps() - // Act renderWithProviders() - // Assert expect(screen.getByTestId('input-field-form')).toBeInTheDocument() }) it('should render close button', () => { - // Arrange const props = createInputFieldEditorProps() - // Act renderWithProviders() - // Assert const closeButton = screen.getByRole('button', { name: '' }) expect(closeButton).toBeInTheDocument() }) it('should render "Add Input Field" title when no initialData', () => { - // Arrange const props = createInputFieldEditorProps({ initialData: undefined }) - // Act renderWithProviders() - // Assert expect( screen.getByText('datasetPipeline.inputFieldPanel.addInputField'), ).toBeInTheDocument() }) it('should render "Edit Input Field" title when initialData is provided', () => { - // Arrange const props = createInputFieldEditorProps({ initialData: createInputVar(), }) - // Act renderWithProviders() - // Assert expect( screen.getByText('datasetPipeline.inputFieldPanel.editInputField'), ).toBeInTheDocument() }) it('should pass supportFile=true to form', () => { - // Arrange const props = createInputFieldEditorProps() - // Act renderWithProviders() - // Assert expect(screen.getByTestId('form-support-file').textContent).toBe('true') }) it('should pass isEditMode=false when no initialData', () => { - // Arrange const props = createInputFieldEditorProps({ initialData: undefined }) - // Act renderWithProviders() - // Assert expect(screen.getByTestId('form-is-edit-mode').textContent).toBe('false') }) it('should pass isEditMode=true when initialData is provided', () => { - // Arrange const props = createInputFieldEditorProps({ initialData: createInputVar(), }) - // Act renderWithProviders() - // Assert expect(screen.getByTestId('form-is-edit-mode').textContent).toBe('true') }) }) - // ------------------------------------------------------------------------- - // Props Variations Tests - // ------------------------------------------------------------------------- describe('Props Variations', () => { it('should handle different input types in initialData', () => { - // Arrange const typesToTest = [ PipelineInputVarType.textInput, PipelineInputVarType.paragraph, @@ -269,19 +222,16 @@ describe('InputFieldEditorPanel', () => { const initialData = createInputVar({ type }) const props = createInputFieldEditorProps({ initialData }) - // Act const { unmount } = renderWithProviders( , ) - // Assert expect(screen.getByTestId('input-field-form')).toBeInTheDocument() unmount() }) }) it('should handle initialData with all optional fields populated', () => { - // Arrange const initialData = createInputVar({ default_value: 'default', tooltips: 'tooltip text', @@ -294,15 +244,12 @@ describe('InputFieldEditorPanel', () => { }) const props = createInputFieldEditorProps({ initialData }) - // Act renderWithProviders() - // Assert expect(screen.getByTestId('input-field-form')).toBeInTheDocument() }) it('should handle initialData with minimal fields', () => { - // Arrange const initialData: InputVar = { type: PipelineInputVarType.textInput, label: 'Min', @@ -311,54 +258,40 @@ describe('InputFieldEditorPanel', () => { } const props = createInputFieldEditorProps({ initialData }) - // Act renderWithProviders() - // Assert expect(screen.getByTestId('input-field-form')).toBeInTheDocument() }) }) - // ------------------------------------------------------------------------- - // User Interaction Tests - // ------------------------------------------------------------------------- describe('User Interactions', () => { it('should call onClose when close button is clicked', () => { - // Arrange const onClose = vi.fn() const props = createInputFieldEditorProps({ onClose }) - // Act renderWithProviders() fireEvent.click(screen.getByTestId('input-field-editor-close-btn')) - // Assert expect(onClose).toHaveBeenCalledTimes(1) }) it('should call onClose when form cancel is triggered', () => { - // Arrange const onClose = vi.fn() const props = createInputFieldEditorProps({ onClose }) - // Act renderWithProviders() fireEvent.click(screen.getByTestId('form-cancel-btn')) - // Assert expect(onClose).toHaveBeenCalledTimes(1) }) it('should call onSubmit with converted data when form submits', () => { - // Arrange const onSubmit = vi.fn() const props = createInputFieldEditorProps({ onSubmit }) - // Act renderWithProviders() fireEvent.click(screen.getByTestId('form-submit-btn')) - // Assert expect(onSubmit).toHaveBeenCalledTimes(1) expect(onSubmit).toHaveBeenCalledWith( expect.objectContaining({ @@ -370,35 +303,26 @@ describe('InputFieldEditorPanel', () => { }) }) - // ------------------------------------------------------------------------- - // Floating Right Behavior Tests - // ------------------------------------------------------------------------- describe('Floating Right Behavior', () => { it('should call useFloatingRight hook', () => { - // Arrange const props = createInputFieldEditorProps() - // Act renderWithProviders() - // Assert expect(mockUseFloatingRight).toHaveBeenCalled() }) it('should apply floating right styles when floatingRight is true', () => { - // Arrange mockUseFloatingRight.mockReturnValue({ floatingRight: true, floatingRightWidth: 300, }) const props = createInputFieldEditorProps() - // Act const { container } = renderWithProviders( , ) - // Assert const panel = container.firstChild as HTMLElement expect(panel.className).toContain('absolute') expect(panel.className).toContain('right-0') @@ -406,35 +330,27 @@ describe('InputFieldEditorPanel', () => { }) it('should not apply floating right styles when floatingRight is false', () => { - // Arrange mockUseFloatingRight.mockReturnValue({ floatingRight: false, floatingRightWidth: 400, }) const props = createInputFieldEditorProps() - // Act const { container } = renderWithProviders( , ) - // Assert const panel = container.firstChild as HTMLElement expect(panel.className).not.toContain('absolute') expect(panel.style.width).toBe('400px') }) }) - // ------------------------------------------------------------------------- - // Callback Stability and Memoization Tests - // ------------------------------------------------------------------------- describe('Callback Stability', () => { it('should maintain stable onClose callback reference', () => { - // Arrange const onClose = vi.fn() const props = createInputFieldEditorProps({ onClose }) - // Act const { rerender } = renderWithProviders( , ) @@ -447,16 +363,13 @@ describe('InputFieldEditorPanel', () => { ) fireEvent.click(screen.getByTestId('form-cancel-btn')) - // Assert expect(onClose).toHaveBeenCalledTimes(2) }) it('should maintain stable onSubmit callback reference', () => { - // Arrange const onSubmit = vi.fn() const props = createInputFieldEditorProps({ onSubmit }) - // Act const { rerender } = renderWithProviders( , ) @@ -469,21 +382,15 @@ describe('InputFieldEditorPanel', () => { ) fireEvent.click(screen.getByTestId('form-submit-btn')) - // Assert expect(onSubmit).toHaveBeenCalledTimes(2) }) }) - // ------------------------------------------------------------------------- - // Memoization Tests - // ------------------------------------------------------------------------- describe('Memoization', () => { it('should memoize formData when initialData does not change', () => { - // Arrange const initialData = createInputVar() const props = createInputFieldEditorProps({ initialData }) - // Act const { rerender } = renderWithProviders( , ) @@ -496,18 +403,15 @@ describe('InputFieldEditorPanel', () => { ) const secondFormData = screen.getByTestId('form-initial-data').textContent - // Assert expect(firstFormData).toBe(secondFormData) }) it('should recompute formData when initialData changes', () => { - // Arrange const initialData1 = createInputVar({ variable: 'var1' }) const initialData2 = createInputVar({ variable: 'var2' }) const props1 = createInputFieldEditorProps({ initialData: initialData1 }) const props2 = createInputFieldEditorProps({ initialData: initialData2 }) - // Act const { rerender } = renderWithProviders( , ) @@ -520,33 +424,25 @@ describe('InputFieldEditorPanel', () => { ) const secondFormData = screen.getByTestId('form-initial-data').textContent - // Assert expect(firstFormData).not.toBe(secondFormData) expect(firstFormData).toContain('var1') expect(secondFormData).toContain('var2') }) }) - // ------------------------------------------------------------------------- - // Edge Cases Tests - // ------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle undefined initialData gracefully', () => { - // Arrange const props = createInputFieldEditorProps({ initialData: undefined }) - // Act & Assert expect(() => renderWithProviders(), ).not.toThrow() }) it('should handle rapid close button clicks', () => { - // Arrange const onClose = vi.fn() const props = createInputFieldEditorProps({ onClose }) - // Act renderWithProviders() const closeButtons = screen.getAllByRole('button') const closeButton = closeButtons.find(btn => btn.querySelector('svg')) @@ -557,12 +453,10 @@ describe('InputFieldEditorPanel', () => { fireEvent.click(closeButton) } - // Assert expect(onClose).toHaveBeenCalledTimes(3) }) it('should handle special characters in initialData', () => { - // Arrange const initialData = createInputVar({ label: 'Test ', variable: 'test_var', @@ -570,15 +464,12 @@ describe('InputFieldEditorPanel', () => { }) const props = createInputFieldEditorProps({ initialData }) - // Act renderWithProviders() - // Assert expect(screen.getByTestId('input-field-form')).toBeInTheDocument() }) it('should handle empty string values in initialData', () => { - // Arrange const initialData = createInputVar({ label: '', variable: '', @@ -588,26 +479,16 @@ describe('InputFieldEditorPanel', () => { }) const props = createInputFieldEditorProps({ initialData }) - // Act renderWithProviders() - // Assert expect(screen.getByTestId('input-field-form')).toBeInTheDocument() }) }) }) -// ============================================================================ -// Utils Tests - convertToInputFieldFormData -// ============================================================================ - describe('convertToInputFieldFormData', () => { - // ------------------------------------------------------------------------- - // Basic Conversion Tests - // ------------------------------------------------------------------------- describe('Basic Conversion', () => { it('should convert InputVar to FormData with all fields', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.textInput, label: 'Test', @@ -621,10 +502,8 @@ describe('convertToInputFieldFormData', () => { unit: 'kg', }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.textInput) expect(result.label).toBe('Test') expect(result.variable).toBe('test_var') @@ -638,7 +517,6 @@ describe('convertToInputFieldFormData', () => { }) it('should convert file-related fields correctly', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.singleFile, allowed_file_upload_methods: ['local_file', 'remote_url'] as TransferMethod[], @@ -646,10 +524,8 @@ describe('convertToInputFieldFormData', () => { allowed_file_extensions: ['.jpg', '.pdf'], }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.allowedFileUploadMethods).toEqual([ 'local_file', 'remote_url', @@ -661,10 +537,8 @@ describe('convertToInputFieldFormData', () => { }) it('should return default template when data is undefined', () => { - // Act const result = convertToInputFieldFormData(undefined) - // Assert expect(result.type).toBe(PipelineInputVarType.textInput) expect(result.variable).toBe('') expect(result.label).toBe('') @@ -672,183 +546,140 @@ describe('convertToInputFieldFormData', () => { }) }) - // ------------------------------------------------------------------------- - // Optional Fields Handling Tests - // ------------------------------------------------------------------------- describe('Optional Fields Handling', () => { it('should not include default when default_value is undefined', () => { - // Arrange const inputVar = createInputVar({ default_value: undefined, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.default).toBeUndefined() }) it('should not include default when default_value is null', () => { - // Arrange const inputVar: InputVar = { ...createInputVar(), default_value: null as unknown as string, } - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.default).toBeUndefined() }) it('should include default when default_value is empty string', () => { - // Arrange const inputVar = createInputVar({ default_value: '', }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.default).toBe('') }) it('should not include tooltips when undefined', () => { - // Arrange const inputVar = createInputVar({ tooltips: undefined, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.tooltips).toBeUndefined() }) it('should not include placeholder when undefined', () => { - // Arrange const inputVar = createInputVar({ placeholder: undefined, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.placeholder).toBeUndefined() }) it('should not include unit when undefined', () => { - // Arrange const inputVar = createInputVar({ unit: undefined, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.unit).toBeUndefined() }) it('should not include file settings when allowed_file_upload_methods is undefined', () => { - // Arrange const inputVar = createInputVar({ allowed_file_upload_methods: undefined, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.allowedFileUploadMethods).toBeUndefined() }) it('should not include allowedTypesAndExtensions details when file types/extensions are missing', () => { - // Arrange const inputVar = createInputVar({ allowed_file_types: undefined, allowed_file_extensions: undefined, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.allowedTypesAndExtensions).toEqual({}) }) }) - // ------------------------------------------------------------------------- - // Type-Specific Tests - // ------------------------------------------------------------------------- describe('Type-Specific Handling', () => { it('should handle textInput type', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.textInput, max_length: 256, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.textInput) expect(result.maxLength).toBe(256) }) it('should handle paragraph type', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.paragraph, max_length: 1000, }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.paragraph) expect(result.maxLength).toBe(1000) }) it('should handle number type with unit', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.number, unit: 'meters', }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.number) expect(result.unit).toBe('meters') }) it('should handle select type with options', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.select, options: ['Option A', 'Option B', 'Option C'], }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.select) expect(result.options).toEqual(['Option A', 'Option B', 'Option C']) }) it('should handle singleFile type', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.singleFile, allowed_file_upload_methods: ['local_file'] as TransferMethod[], @@ -856,16 +687,13 @@ describe('convertToInputFieldFormData', () => { allowed_file_extensions: ['.jpg'], }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.singleFile) expect(result.allowedFileUploadMethods).toEqual(['local_file']) }) it('should handle multiFiles type', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.multiFiles, max_length: 5, @@ -874,42 +702,29 @@ describe('convertToInputFieldFormData', () => { allowed_file_extensions: ['.pdf', '.doc'], }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.multiFiles) expect(result.maxLength).toBe(5) }) it('should handle checkbox type', () => { - // Arrange const inputVar = createInputVar({ type: PipelineInputVarType.checkbox, default_value: 'true', }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.type).toBe(PipelineInputVarType.checkbox) expect(result.default).toBe('true') }) }) }) -// ============================================================================ -// Utils Tests - convertFormDataToINputField -// ============================================================================ - describe('convertFormDataToINputField', () => { - // ------------------------------------------------------------------------- - // Basic Conversion Tests - // ------------------------------------------------------------------------- describe('Basic Conversion', () => { it('should convert FormData to InputVar with all fields', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.textInput, label: 'Test', @@ -928,10 +743,8 @@ describe('convertFormDataToINputField', () => { }, }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.textInput) expect(result.label).toBe('Test') expect(result.variable).toBe('test_var') @@ -948,7 +761,6 @@ describe('convertFormDataToINputField', () => { }) it('should handle undefined optional fields', () => { - // Arrange const formData = createFormData({ default: undefined, tooltips: undefined, @@ -961,10 +773,8 @@ describe('convertFormDataToINputField', () => { }, }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.default_value).toBeUndefined() expect(result.tooltips).toBeUndefined() expect(result.placeholder).toBeUndefined() @@ -975,42 +785,30 @@ describe('convertFormDataToINputField', () => { }) }) - // ------------------------------------------------------------------------- - // Field Mapping Tests - // ------------------------------------------------------------------------- describe('Field Mapping', () => { it('should map maxLength to max_length', () => { - // Arrange const formData = createFormData({ maxLength: 256 }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.max_length).toBe(256) }) it('should map default to default_value', () => { - // Arrange const formData = createFormData({ default: 'my default' }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.default_value).toBe('my default') }) it('should map allowedFileUploadMethods to allowed_file_upload_methods', () => { - // Arrange const formData = createFormData({ allowedFileUploadMethods: ['local_file', 'remote_url'] as TransferMethod[], }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.allowed_file_upload_methods).toEqual([ 'local_file', 'remote_url', @@ -1018,7 +816,6 @@ describe('convertFormDataToINputField', () => { }) it('should map allowedTypesAndExtensions to separate fields', () => { - // Arrange const formData = createFormData({ allowedTypesAndExtensions: { allowedFileTypes: ['image', 'document'] as SupportUploadFileTypes[], @@ -1026,119 +823,88 @@ describe('convertFormDataToINputField', () => { }, }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.allowed_file_types).toEqual(['image', 'document']) expect(result.allowed_file_extensions).toEqual(['.jpg', '.pdf']) }) }) - // ------------------------------------------------------------------------- - // Type-Specific Tests - // ------------------------------------------------------------------------- describe('Type-Specific Handling', () => { it('should preserve textInput type', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.textInput }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.textInput) }) it('should preserve paragraph type', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.paragraph }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.paragraph) }) it('should preserve select type with options', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.select, options: ['A', 'B', 'C'], }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.select) expect(result.options).toEqual(['A', 'B', 'C']) }) it('should preserve number type with unit', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.number, unit: 'kg', }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.number) expect(result.unit).toBe('kg') }) it('should preserve singleFile type', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.singleFile, }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.singleFile) }) it('should preserve multiFiles type with maxLength', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.multiFiles, maxLength: 10, }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.multiFiles) expect(result.max_length).toBe(10) }) it('should preserve checkbox type', () => { - // Arrange const formData = createFormData({ type: PipelineInputVarType.checkbox }) - // Act const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(PipelineInputVarType.checkbox) }) }) }) -// ============================================================================ -// Round-Trip Conversion Tests -// ============================================================================ - describe('Round-Trip Conversion', () => { it('should preserve data through round-trip conversion for textInput', () => { - // Arrange const original = createInputVar({ type: PipelineInputVarType.textInput, label: 'Test Label', @@ -1150,11 +916,9 @@ describe('Round-Trip Conversion', () => { placeholder: 'placeholder', }) - // Act const formData = convertToInputFieldFormData(original) const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(original.type) expect(result.label).toBe(original.label) expect(result.variable).toBe(original.variable) @@ -1166,25 +930,21 @@ describe('Round-Trip Conversion', () => { }) it('should preserve data through round-trip conversion for select', () => { - // Arrange const original = createInputVar({ type: PipelineInputVarType.select, options: ['Option A', 'Option B', 'Option C'], default_value: 'Option A', }) - // Act const formData = convertToInputFieldFormData(original) const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(original.type) expect(result.options).toEqual(original.options) expect(result.default_value).toBe(original.default_value) }) it('should preserve data through round-trip conversion for file types', () => { - // Arrange const original = createInputVar({ type: PipelineInputVarType.multiFiles, max_length: 5, @@ -1193,11 +953,9 @@ describe('Round-Trip Conversion', () => { allowed_file_extensions: ['.jpg', '.pdf'], }) - // Act const formData = convertToInputFieldFormData(original) const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(original.type) expect(result.max_length).toBe(original.max_length) expect(result.allowed_file_upload_methods).toEqual( @@ -1210,7 +968,6 @@ describe('Round-Trip Conversion', () => { }) it('should handle all input types through round-trip', () => { - // Arrange const typesToTest = [ PipelineInputVarType.textInput, PipelineInputVarType.paragraph, @@ -1224,54 +981,39 @@ describe('Round-Trip Conversion', () => { typesToTest.forEach((type) => { const original = createInputVar({ type }) - // Act const formData = convertToInputFieldFormData(original) const result = convertFormDataToINputField(formData) - // Assert expect(result.type).toBe(original.type) }) }) }) -// ============================================================================ -// Edge Cases Tests -// ============================================================================ - describe('Edge Cases', () => { describe('convertToInputFieldFormData edge cases', () => { it('should handle zero maxLength', () => { - // Arrange const inputVar = createInputVar({ max_length: 0 }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.maxLength).toBe(0) }) it('should handle empty options array', () => { - // Arrange const inputVar = createInputVar({ options: [] }) - // Act const result = convertToInputFieldFormData(inputVar) - // Assert expect(result.options).toEqual([]) }) it('should handle options with special characters', () => { - // Arrange const inputVar = createInputVar({ options: ['' }, { content: '中文内容 🎉' }, { content: 'Line1\nLine2\tTab' }, ]) - // Act const result = formatPreviewChunks(outputs) as GeneralChunks - // Assert expect(result).toEqual([ { content: '', summary: undefined }, { content: '中文内容 🎉', summary: undefined }, @@ -212,34 +162,25 @@ describe('formatPreviewChunks', () => { }) it('should handle general chunks with very long content', () => { - // Arrange const longContent = 'A'.repeat(10000) const outputs = createGeneralChunkOutputs([{ content: longContent }]) - // Act const result = formatPreviewChunks(outputs) as GeneralChunks - // Assert expect((result as GeneralChunks)[0].content).toHaveLength(10000) }) }) - // ------------------------------------------------------------------------- - // Parent-Child Chunks (hierarchical_model) Tests - // ------------------------------------------------------------------------- describe('Parent-Child Chunks (hierarchical_model)', () => { describe('Paragraph Mode', () => { it('should format parent-child chunks in paragraph mode correctly', () => { - // Arrange const outputs = createParentChildChunkOutputs([ { content: 'Parent 1', child_chunks: ['Child 1-1', 'Child 1-2'] }, { content: 'Parent 2', child_chunks: ['Child 2-1'] }, ], 'paragraph') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_mode).toBe('paragraph') expect(result.parent_child_chunks).toHaveLength(2) expect(result.parent_child_chunks[0]).toEqual({ @@ -255,54 +196,42 @@ describe('formatPreviewChunks', () => { }) it('should limit parent chunks to RAG_PIPELINE_PREVIEW_CHUNK_NUM (20) in paragraph mode', () => { - // Arrange const outputs = createParentChildChunkOutputs(createMockParentChildChunks(30, 2), 'paragraph') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_child_chunks).toHaveLength(20) }) it('should NOT limit child chunks in paragraph mode', () => { - // Arrange const outputs = createParentChildChunkOutputs([ { content: 'Parent 1', child_chunks: Array.from({ length: 50 }, (_, i) => `Child ${i + 1}`) }, ], 'paragraph') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_child_chunks[0].child_contents).toHaveLength(50) }) it('should handle empty child_chunks in paragraph mode', () => { - // Arrange const outputs = createParentChildChunkOutputs([ { content: 'Parent with no children', child_chunks: [] }, ], 'paragraph') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_child_chunks[0].child_contents).toEqual([]) }) }) describe('Full-Doc Mode', () => { it('should format parent-child chunks in full-doc mode correctly', () => { - // Arrange const outputs = createParentChildChunkOutputs([ { content: 'Full Doc Parent', child_chunks: ['Child 1', 'Child 2', 'Child 3'] }, ], 'full-doc') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_mode).toBe('full-doc') expect(result.parent_child_chunks).toHaveLength(1) expect(result.parent_child_chunks[0].parent_content).toBe('Full Doc Parent') @@ -310,74 +239,56 @@ describe('formatPreviewChunks', () => { }) it('should NOT limit parent chunks in full-doc mode', () => { - // Arrange const outputs = createParentChildChunkOutputs(createMockParentChildChunks(30, 2), 'full-doc') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert - full-doc mode processes all parents (forEach without slice) expect(result.parent_child_chunks).toHaveLength(30) }) it('should limit child chunks to RAG_PIPELINE_PREVIEW_CHUNK_NUM (20) in full-doc mode', () => { - // Arrange const outputs = createParentChildChunkOutputs([ { content: 'Parent', child_chunks: Array.from({ length: 50 }, (_, i) => `Child ${i + 1}`) }, ], 'full-doc') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_child_chunks[0].child_contents).toHaveLength(20) expect(result.parent_child_chunks[0].child_contents[0]).toBe('Child 1') expect(result.parent_child_chunks[0].child_contents[19]).toBe('Child 20') }) it('should handle multiple parents with many children in full-doc mode', () => { - // Arrange const outputs = createParentChildChunkOutputs([ { content: 'Parent 1', child_chunks: Array.from({ length: 25 }, (_, i) => `P1-Child ${i + 1}`) }, { content: 'Parent 2', child_chunks: Array.from({ length: 30 }, (_, i) => `P2-Child ${i + 1}`) }, ], 'full-doc') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_child_chunks[0].child_contents).toHaveLength(20) expect(result.parent_child_chunks[1].child_contents).toHaveLength(20) }) }) it('should handle empty preview array for parent-child chunks', () => { - // Arrange const outputs = createParentChildChunkOutputs([], 'paragraph') - // Act const result = formatPreviewChunks(outputs) as ParentChildChunks - // Assert expect(result.parent_child_chunks).toEqual([]) }) }) - // ------------------------------------------------------------------------- - // QA Chunks (qa_model) Tests - // ------------------------------------------------------------------------- describe('QA Chunks (qa_model)', () => { it('should format QA chunks correctly', () => { - // Arrange const outputs = createQAChunkOutputs([ { question: 'What is Dify?', answer: 'Dify is an LLM application platform.' }, { question: 'How to use it?', answer: 'You can create apps easily.' }, ]) - // Act const result = formatPreviewChunks(outputs) as QAChunks - // Assert expect(result.qa_chunks).toHaveLength(2) expect(result.qa_chunks[0]).toEqual({ question: 'What is Dify?', @@ -390,38 +301,29 @@ describe('formatPreviewChunks', () => { }) it('should limit QA chunks to RAG_PIPELINE_PREVIEW_CHUNK_NUM (20)', () => { - // Arrange const outputs = createQAChunkOutputs(createMockQAChunks(30)) - // Act const result = formatPreviewChunks(outputs) as QAChunks - // Assert expect(result.qa_chunks).toHaveLength(20) }) it('should handle empty qa_preview array', () => { - // Arrange const outputs = createQAChunkOutputs([]) - // Act const result = formatPreviewChunks(outputs) as QAChunks - // Assert expect(result.qa_chunks).toEqual([]) }) it('should handle QA chunks with empty question or answer', () => { - // Arrange const outputs = createQAChunkOutputs([ { question: '', answer: 'Answer without question' }, { question: 'Question without answer', answer: '' }, ]) - // Act const result = formatPreviewChunks(outputs) as QAChunks - // Assert expect(result.qa_chunks[0].question).toBe('') expect(result.qa_chunks[0].answer).toBe('Answer without question') expect(result.qa_chunks[1].question).toBe('Question without answer') @@ -429,7 +331,6 @@ describe('formatPreviewChunks', () => { }) it('should preserve all properties when spreading chunk', () => { - // Arrange const outputs = { chunk_structure: ChunkingMode.qa, qa_preview: [ @@ -437,90 +338,63 @@ describe('formatPreviewChunks', () => { ] as unknown as Array<{ question: string, answer: string }>, } - // Act const result = formatPreviewChunks(outputs) as QAChunks - // Assert expect(result.qa_chunks[0]).toEqual({ question: 'Q1', answer: 'A1', extra: 'should be preserved' }) }) }) - // ------------------------------------------------------------------------- - // Unknown Chunking Mode Tests - // ------------------------------------------------------------------------- describe('Unknown Chunking Mode', () => { it('should return undefined for unknown chunking mode', () => { - // Arrange const outputs = { chunk_structure: 'unknown_mode' as ChunkingMode, preview: [], } - // Act const result = formatPreviewChunks(outputs) - // Assert expect(result).toBeUndefined() }) it('should return undefined when chunk_structure is missing', () => { - // Arrange const outputs = { preview: [{ content: 'test' }], } - // Act const result = formatPreviewChunks(outputs) - // Assert expect(result).toBeUndefined() }) }) - // ------------------------------------------------------------------------- - // Edge Cases Tests - // ------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle exactly RAG_PIPELINE_PREVIEW_CHUNK_NUM (20) chunks', () => { - // Arrange const outputs = createGeneralChunkOutputs(createMockGeneralChunks(20)) - // Act const result = formatPreviewChunks(outputs) as GeneralChunks - // Assert expect(result).toHaveLength(20) }) it('should handle outputs with additional properties', () => { - // Arrange const outputs = { ...createGeneralChunkOutputs([{ content: 'Test' }]), extra_field: 'should not affect result', metadata: { some: 'data' }, } - // Act const result = formatPreviewChunks(outputs) as GeneralChunks - // Assert expect(result).toEqual([{ content: 'Test', summary: undefined }]) }) }) }) -// ============================================================================ -// ResultPreview Component Tests -// ============================================================================ - describe('ResultPreview', () => { beforeEach(() => { vi.clearAllMocks() }) - // ------------------------------------------------------------------------- - // Default Props Factory - // ------------------------------------------------------------------------- const defaultProps = { isRunning: false, outputs: undefined, @@ -528,117 +402,85 @@ describe('ResultPreview', () => { onSwitchToDetail: vi.fn(), } - // ------------------------------------------------------------------------- - // Rendering Tests - // ------------------------------------------------------------------------- describe('Rendering', () => { it('should render without crashing with minimal props', () => { - // Arrange & Act render() - // Assert - Component renders (no visible content in empty state) expect(document.body).toBeInTheDocument() }) it('should render loading state when isRunning and no outputs', () => { - // Arrange & Act render() - // Assert expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument() }) it('should render loading spinner icon when loading', () => { - // Arrange & Act const { container } = render() - // Assert - Check for animate-spin class (loading spinner) const spinner = container.querySelector('.animate-spin') expect(spinner).toBeInTheDocument() }) it('should render error state when not running and error exists', () => { - // Arrange & Act render() - // Assert expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument() expect(screen.getByRole('button', { name: /pipeline\.result\.resultPreview\.viewDetails/i })).toBeInTheDocument() }) it('should render outputs when available', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Test chunk' }]) - // Act render() - // Assert expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) it('should render footer tip when outputs available', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Test chunk' }]) - // Act render() - // Assert expect(screen.getByText(/pipeline\.result\.resultPreview\.footerTip/)).toBeInTheDocument() }) it('should not render loading when outputs exist even if isRunning', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Test' }]) - // Act render() - // Assert - Should show outputs, not loading expect(screen.queryByText('pipeline.result.resultPreview.loading')).not.toBeInTheDocument() expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) it('should not render error when isRunning is true', () => { - // Arrange & Act render() - // Assert - Should show loading, not error expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument() expect(screen.queryByText('pipeline.result.resultPreview.error')).not.toBeInTheDocument() }) }) - // ------------------------------------------------------------------------- - // Props Variations Tests - // ------------------------------------------------------------------------- describe('Props Variations', () => { describe('isRunning prop', () => { it('should show loading when isRunning=true and no outputs', () => { - // Arrange & Act render() - // Assert expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument() }) it('should not show loading when isRunning=false', () => { - // Arrange & Act render() - // Assert expect(screen.queryByText('pipeline.result.resultPreview.loading')).not.toBeInTheDocument() }) it('should prioritize outputs over loading state', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Data' }]) - // Act render() - // Assert expect(screen.queryByText('pipeline.result.resultPreview.loading')).not.toBeInTheDocument() expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) @@ -646,28 +488,22 @@ describe('ResultPreview', () => { describe('outputs prop', () => { it('should pass chunk_structure to ChunkCardList', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Test' }]) - // Act render() - // Assert const chunkList = screen.getByTestId('chunk-card-list') expect(chunkList).toHaveAttribute('data-chunk-type', ChunkingMode.text) }) it('should format and pass previewChunks to ChunkCardList', () => { - // Arrange const outputs = createGeneralChunkOutputs([ { content: 'Chunk 1' }, { content: 'Chunk 2' }, ]) - // Act render() - // Assert const chunkList = screen.getByTestId('chunk-card-list') const chunkInfo = JSON.parse(chunkList.getAttribute('data-chunk-info') || '[]') expect(chunkInfo).toEqual([ @@ -677,29 +513,23 @@ describe('ResultPreview', () => { }) it('should handle parent-child outputs', () => { - // Arrange const outputs = createParentChildChunkOutputs([ { content: 'Parent', child_chunks: ['Child 1', 'Child 2'] }, ]) - // Act render() - // Assert const chunkList = screen.getByTestId('chunk-card-list') expect(chunkList).toHaveAttribute('data-chunk-type', ChunkingMode.parentChild) }) it('should handle QA outputs', () => { - // Arrange const outputs = createQAChunkOutputs([ { question: 'Q1', answer: 'A1' }, ]) - // Act render() - // Assert const chunkList = screen.getByTestId('chunk-card-list') expect(chunkList).toHaveAttribute('data-chunk-type', ChunkingMode.qa) }) @@ -707,29 +537,22 @@ describe('ResultPreview', () => { describe('error prop', () => { it('should show error state when error is a non-empty string', () => { - // Arrange & Act render() - // Assert expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument() }) it('should show error state when error is an empty string', () => { - // Arrange & Act render() - // Assert - Empty string is falsy, so error state should NOT show expect(screen.queryByText('pipeline.result.resultPreview.error')).not.toBeInTheDocument() }) it('should render both outputs and error when both exist (independent conditions)', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Data' }]) - // Act render() - // Assert - Both are rendered because conditions are independent in the component expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument() expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) @@ -737,65 +560,48 @@ describe('ResultPreview', () => { describe('onSwitchToDetail prop', () => { it('should be called when view details button is clicked', () => { - // Arrange const onSwitchToDetail = vi.fn() render() - // Act fireEvent.click(screen.getByRole('button', { name: /viewDetails/i })) - // Assert expect(onSwitchToDetail).toHaveBeenCalledTimes(1) }) it('should not be called automatically on render', () => { - // Arrange const onSwitchToDetail = vi.fn() - // Act render() - // Assert expect(onSwitchToDetail).not.toHaveBeenCalled() }) }) }) - // ------------------------------------------------------------------------- - // Memoization Tests - // ------------------------------------------------------------------------- describe('Memoization', () => { describe('React.memo wrapper', () => { it('should be wrapped with React.memo', () => { - // Arrange & Act const { rerender } = render() rerender() - // Assert - Component renders correctly after rerender expect(document.body).toBeInTheDocument() }) it('should update when props change', () => { - // Arrange const { rerender } = render() - // Act rerender() - // Assert expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument() }) it('should update when outputs change', () => { - // Arrange const outputs1 = createGeneralChunkOutputs([{ content: 'First' }]) const { rerender } = render() - // Act const outputs2 = createGeneralChunkOutputs([{ content: 'Second' }]) rerender() - // Assert const chunkList = screen.getByTestId('chunk-card-list') const chunkInfo = JSON.parse(chunkList.getAttribute('data-chunk-info') || '[]') expect(chunkInfo).toEqual([{ content: 'Second' }]) @@ -804,23 +610,19 @@ describe('ResultPreview', () => { describe('useMemo for previewChunks', () => { it('should compute previewChunks based on outputs', () => { - // Arrange const outputs = createGeneralChunkOutputs([ { content: 'Memoized chunk 1' }, { content: 'Memoized chunk 2' }, ]) - // Act render() - // Assert const chunkList = screen.getByTestId('chunk-card-list') const chunkInfo = JSON.parse(chunkList.getAttribute('data-chunk-info') || '[]') expect(chunkInfo).toHaveLength(2) }) it('should recompute when outputs reference changes', () => { - // Arrange const outputs1 = createGeneralChunkOutputs([{ content: 'Original' }]) const { rerender } = render() @@ -828,64 +630,47 @@ describe('ResultPreview', () => { let chunkInfo = JSON.parse(chunkList.getAttribute('data-chunk-info') || '[]') expect(chunkInfo).toEqual([{ content: 'Original' }]) - // Act - Change outputs const outputs2 = createGeneralChunkOutputs([{ content: 'Updated' }]) rerender() - // Assert chunkList = screen.getByTestId('chunk-card-list') chunkInfo = JSON.parse(chunkList.getAttribute('data-chunk-info') || '[]') expect(chunkInfo).toEqual([{ content: 'Updated' }]) }) it('should handle undefined outputs in useMemo', () => { - // Arrange & Act render() - // Assert - No chunk list rendered expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument() }) }) }) - // ------------------------------------------------------------------------- - // Event Handlers Tests - // ------------------------------------------------------------------------- describe('Event Handlers', () => { it('should call onSwitchToDetail when view details button is clicked', () => { - // Arrange const onSwitchToDetail = vi.fn() render() - // Act fireEvent.click(screen.getByRole('button', { name: /viewDetails/i })) - // Assert expect(onSwitchToDetail).toHaveBeenCalledTimes(1) }) it('should handle multiple clicks on view details button', () => { - // Arrange const onSwitchToDetail = vi.fn() render() const button = screen.getByRole('button', { name: /viewDetails/i }) - // Act fireEvent.click(button) fireEvent.click(button) fireEvent.click(button) - // Assert expect(onSwitchToDetail).toHaveBeenCalledTimes(3) }) }) - // ------------------------------------------------------------------------- - // Edge Cases Tests - // ------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle empty state (all props undefined/false)', () => { - // Arrange & Act const { container } = render( { />, ) - // Assert - Should render empty fragment expect(container.firstChild).toBeNull() }) it('should handle outputs with empty preview chunks', () => { - // Arrange const outputs = createGeneralChunkOutputs([]) - // Act render() - // Assert const chunkList = screen.getByTestId('chunk-card-list') const chunkInfo = JSON.parse(chunkList.getAttribute('data-chunk-info') || '[]') expect(chunkInfo).toEqual([]) }) it('should handle outputs that result in undefined previewChunks', () => { - // Arrange const outputs = { chunk_structure: 'invalid_mode' as ChunkingMode, preview: [], } - // Act render() - // Assert - Should not render chunk list when previewChunks is undefined expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument() }) it('should handle unmount cleanly', () => { - // Arrange const { unmount } = render() - // Assert expect(() => unmount()).not.toThrow() }) it('should handle rapid prop changes', () => { - // Arrange const { rerender } = render() - // Act - Rapidly change props rerender() rerender() rerender() rerender() - // Assert expect(screen.queryByTestId('chunk-card-list')).not.toBeInTheDocument() }) it('should handle very large number of chunks', () => { - // Arrange const outputs = createGeneralChunkOutputs(createMockGeneralChunks(1000)) - // Act render() - // Assert - Should only show first 20 chunks const chunkList = screen.getByTestId('chunk-card-list') const chunkInfo = JSON.parse(chunkList.getAttribute('data-chunk-info') || '[]') expect(chunkInfo).toHaveLength(20) }) it('should throw when outputs has null preview (slice called on null)', () => { - // Arrange const outputs = { chunk_structure: ChunkingMode.text, preview: null as unknown as Array<{ content: string }>, } - // Act & Assert - Component throws because slice is called on null preview - // This is expected behavior - the component doesn't validate input expect(() => render()).toThrow() }) }) - // ------------------------------------------------------------------------- - // Integration Tests - // ------------------------------------------------------------------------- describe('Integration', () => { it('should transition from loading to output state', () => { - // Arrange const { rerender } = render() expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument() - // Act const outputs = createGeneralChunkOutputs([{ content: 'Loaded data' }]) rerender() - // Assert expect(screen.queryByText('pipeline.result.resultPreview.loading')).not.toBeInTheDocument() expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) it('should transition from loading to error state', () => { - // Arrange const { rerender } = render() expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument() - // Act rerender() - // Assert expect(screen.queryByText('pipeline.result.resultPreview.loading')).not.toBeInTheDocument() expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument() }) it('should render both error and outputs when both props provided', () => { - // Arrange const { rerender } = render() expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument() - // Act - Outputs provided while error still exists const outputs = createGeneralChunkOutputs([{ content: 'Success data' }]) rerender() - // Assert - Both are rendered (component uses independent conditions) expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument() expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) it('should hide error when error prop is cleared', () => { - // Arrange const { rerender } = render() expect(screen.getByText('pipeline.result.resultPreview.error')).toBeInTheDocument() - // Act - Clear error and provide outputs const outputs = createGeneralChunkOutputs([{ content: 'Success data' }]) rerender() - // Assert - Only outputs shown when error is cleared expect(screen.queryByText('pipeline.result.resultPreview.error')).not.toBeInTheDocument() expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) it('should handle complete flow: empty -> loading -> outputs', () => { - // Arrange const { rerender, container } = render() expect(container.firstChild).toBeNull() - // Act - Start loading rerender() expect(screen.getByText('pipeline.result.resultPreview.loading')).toBeInTheDocument() - // Act - Receive outputs const outputs = createGeneralChunkOutputs([{ content: 'Final data' }]) rerender() - // Assert expect(screen.getByTestId('chunk-card-list')).toBeInTheDocument() }) }) - // ------------------------------------------------------------------------- - // Styling Tests - // ------------------------------------------------------------------------- describe('Styling', () => { it('should have correct container classes for loading state', () => { - // Arrange & Act const { container } = render() - // Assert const loadingContainer = container.querySelector('.flex.grow.flex-col.items-center.justify-center') expect(loadingContainer).toBeInTheDocument() }) it('should have correct container classes for error state', () => { - // Arrange & Act const { container } = render() - // Assert const errorContainer = container.querySelector('.flex.grow.flex-col.items-center.justify-center') expect(errorContainer).toBeInTheDocument() }) it('should have correct container classes for outputs state', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Test' }]) - // Act const { container } = render() - // Assert const outputContainer = container.querySelector('.flex.grow.flex-col.bg-background-body') expect(outputContainer).toBeInTheDocument() }) it('should have gradient dividers in footer', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Test' }]) - // Act const { container } = render() - // Assert const gradientDividers = container.querySelectorAll('.bg-gradient-to-r, .bg-gradient-to-l') expect(gradientDividers.length).toBeGreaterThanOrEqual(2) }) }) - // ------------------------------------------------------------------------- - // Accessibility Tests - // ------------------------------------------------------------------------- describe('Accessibility', () => { it('should have accessible button in error state', () => { - // Arrange & Act render() - // Assert const button = screen.getByRole('button') expect(button).toBeInTheDocument() }) it('should have title attribute on footer tip for long text', () => { - // Arrange const outputs = createGeneralChunkOutputs([{ content: 'Test' }]) - // Act const { container } = render() - // Assert const footerTip = container.querySelector('[title]') expect(footerTip).toBeInTheDocument() }) }) }) -// ============================================================================ -// State Transition Matrix Tests -// ============================================================================ - describe('State Transition Matrix', () => { beforeEach(() => { vi.clearAllMocks() @@ -1147,7 +870,6 @@ describe('State Transition Matrix', () => { it.each(states)( 'should render $expected state when isRunning=$isRunning, outputs=$outputs, error=$error', ({ isRunning, outputs, error, expected }) => { - // Arrange & Act const { container } = render( { />, ) - // Assert switch (expected) { case 'empty': expect(container.firstChild).toBeNull() diff --git a/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/index.spec.tsx b/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/__tests__/index.spec.tsx similarity index 81% rename from web/app/components/rag-pipeline/components/panel/test-run/result/tabs/index.spec.tsx rename to web/app/components/rag-pipeline/components/panel/test-run/result/tabs/__tests__/index.spec.tsx index ec7d404f6e..65dfd06cf6 100644 --- a/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/panel/test-run/result/tabs/__tests__/index.spec.tsx @@ -1,25 +1,8 @@ import type { WorkflowRunningData } from '@/app/components/workflow/types' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import Tabs from './index' -import Tab from './tab' - -// ============================================================================ -// Mock External Dependencies -// ============================================================================ - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string }) => { - const ns = options?.ns ? `${options.ns}.` : '' - return `${ns}${key}` - }, - }), -})) - -// ============================================================================ -// Test Data Factories -// ============================================================================ +import Tabs from '../index' +import Tab from '../tab' /** * Factory function to create mock WorkflowRunningData @@ -52,25 +35,16 @@ const createWorkflowRunningData = ( ...overrides, }) -// ============================================================================ -// Tab Component Tests -// ============================================================================ - describe('Tab', () => { beforeEach(() => { vi.clearAllMocks() }) - // ------------------------------------------------------------------------- - // Rendering Tests - Verify basic component rendering - // ------------------------------------------------------------------------- describe('Rendering', () => { it('should render tab with label correctly', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert expect(screen.getByRole('button', { name: 'Test Label' })).toBeInTheDocument() }) it('should render as button element with correct type', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const button = screen.getByRole('button') expect(button).toHaveAttribute('type', 'button') }) }) - // ------------------------------------------------------------------------- - // Props Tests - Verify different prop combinations - // ------------------------------------------------------------------------- describe('Props', () => { describe('isActive prop', () => { it('should apply active styles when isActive is true', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const button = screen.getByRole('button') expect(button).toHaveClass('border-util-colors-blue-brand-blue-brand-600') expect(button).toHaveClass('text-text-primary') }) it('should apply inactive styles when isActive is false', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const button = screen.getByRole('button') expect(button).toHaveClass('text-text-tertiary') expect(button).toHaveClass('border-transparent') @@ -159,11 +120,9 @@ describe('Tab', () => { describe('label prop', () => { it('should display the provided label text', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert expect(screen.getByText('Custom Label Text')).toBeInTheDocument() }) it('should handle empty label', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert expect(screen.getByRole('button')).toBeInTheDocument() expect(screen.getByRole('button')).toHaveTextContent('') }) it('should handle long label text', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() const longLabel = 'This is a very long label text for testing purposes' - // Act render( { />, ) - // Assert expect(screen.getByText(longLabel)).toBeInTheDocument() }) }) describe('value prop', () => { it('should pass value to onClick handler when clicked', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() const testValue = 'CUSTOM_VALUE' - // Act render( { ) fireEvent.click(screen.getByRole('button')) - // Assert expect(mockOnClick).toHaveBeenCalledWith(testValue) }) }) describe('workflowRunningData prop', () => { it('should enable button when workflowRunningData is provided', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert expect(screen.getByRole('button')).not.toBeDisabled() }) it('should disable button when workflowRunningData is undefined', () => { - // Arrange const mockOnClick = vi.fn() - // Act render( { />, ) - // Assert expect(screen.getByRole('button')).toBeDisabled() }) it('should apply disabled styles when workflowRunningData is undefined', () => { - // Arrange const mockOnClick = vi.fn() - // Act render( { />, ) - // Assert const button = screen.getByRole('button') expect(button).toHaveClass('!cursor-not-allowed') expect(button).toHaveClass('opacity-30') }) it('should not have disabled styles when workflowRunningData is provided', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const button = screen.getByRole('button') expect(button).not.toHaveClass('!cursor-not-allowed') expect(button).not.toHaveClass('opacity-30') @@ -330,16 +267,11 @@ describe('Tab', () => { }) }) - // ------------------------------------------------------------------------- - // Event Handlers Tests - Verify click behavior - // ------------------------------------------------------------------------- describe('Event Handlers', () => { it('should call onClick with value when clicked', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { ) fireEvent.click(screen.getByRole('button')) - // Assert expect(mockOnClick).toHaveBeenCalledTimes(1) expect(mockOnClick).toHaveBeenCalledWith('RESULT') }) it('should not call onClick when disabled (no workflowRunningData)', () => { - // Arrange const mockOnClick = vi.fn() - // Act render( { ) fireEvent.click(screen.getByRole('button')) - // Assert expect(mockOnClick).not.toHaveBeenCalled() }) it('should handle multiple clicks correctly', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { fireEvent.click(button) fireEvent.click(button) - // Assert expect(mockOnClick).toHaveBeenCalledTimes(3) }) }) - // ------------------------------------------------------------------------- - // Memoization Tests - Verify React.memo optimization - // ------------------------------------------------------------------------- describe('Memoization', () => { it('should not re-render when props are the same', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() const renderSpy = vi.fn() @@ -417,7 +338,6 @@ describe('Tab', () => { } const MemoizedTabWithSpy = React.memo(TabWithSpy) - // Act const { rerender } = render( { />, ) - // Re-render with same props rerender( { />, ) - // Assert - React.memo should prevent re-render with same props expect(renderSpy).toHaveBeenCalledTimes(1) }) it('should re-render when isActive prop changes', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act const { rerender } = render( { />, ) - // Assert initial state expect(screen.getByRole('button')).toHaveClass('text-text-tertiary') - // Rerender with changed prop rerender( { />, ) - // Assert updated state expect(screen.getByRole('button')).toHaveClass('text-text-primary') }) it('should re-render when label prop changes', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act const { rerender } = render( { />, ) - // Assert initial state expect(screen.getByText('Original Label')).toBeInTheDocument() - // Rerender with changed prop rerender( { />, ) - // Assert updated state expect(screen.getByText('Updated Label')).toBeInTheDocument() expect(screen.queryByText('Original Label')).not.toBeInTheDocument() }) it('should use stable handleClick callback with useCallback', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act const { rerender } = render( { fireEvent.click(screen.getByRole('button')) expect(mockOnClick).toHaveBeenCalledWith('TEST_VALUE') - // Rerender with same value and onClick rerender( { }) }) - // ------------------------------------------------------------------------- - // Edge Cases Tests - Verify boundary conditions - // ------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle special characters in label', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() const specialLabel = 'Tab <>&"\'' - // Act render( { />, ) - // Assert expect(screen.getByText(specialLabel)).toBeInTheDocument() }) it('should handle special characters in value', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { ) fireEvent.click(screen.getByRole('button')) - // Assert expect(mockOnClick).toHaveBeenCalledWith('SPECIAL_VALUE_123') }) it('should handle unicode in label', () => { - // Arrange const mockOnClick = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert expect(screen.getByText('结果 🚀')).toBeInTheDocument() }) it('should combine isActive and disabled states correctly', () => { - // Arrange const mockOnClick = vi.fn() - // Act - Active but disabled (no workflowRunningData) render( { />, ) - // Assert const button = screen.getByRole('button') expect(button).toBeDisabled() expect(button).toHaveClass('border-util-colors-blue-brand-blue-brand-600') @@ -639,25 +529,16 @@ describe('Tab', () => { }) }) -// ============================================================================ -// Tabs Component Tests -// ============================================================================ - describe('Tabs', () => { beforeEach(() => { vi.clearAllMocks() }) - // ------------------------------------------------------------------------- - // Rendering Tests - Verify basic component rendering - // ------------------------------------------------------------------------- describe('Rendering', () => { it('should render all three tabs', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert - Check all three tabs are rendered with i18n keys expect(screen.getByRole('button', { name: 'runLog.result' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'runLog.detail' })).toBeInTheDocument() expect(screen.getByRole('button', { name: 'runLog.tracing' })).toBeInTheDocument() }) it('should render container with correct styles', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act const { container } = render( { />, ) - // Assert const tabsContainer = container.firstChild expect(tabsContainer).toHaveClass('flex') expect(tabsContainer).toHaveClass('shrink-0') @@ -698,11 +575,9 @@ describe('Tabs', () => { }) it('should render exactly three tab buttons', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(3) }) }) - // ------------------------------------------------------------------------- - // Props Tests - Verify different prop combinations - // ------------------------------------------------------------------------- describe('Props', () => { describe('currentTab prop', () => { it('should set RESULT tab as active when currentTab is RESULT', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const resultTab = screen.getByRole('button', { name: 'runLog.result' }) const detailTab = screen.getByRole('button', { name: 'runLog.detail' }) const tracingTab = screen.getByRole('button', { name: 'runLog.tracing' }) @@ -747,11 +615,9 @@ describe('Tabs', () => { }) it('should set DETAIL tab as active when currentTab is DETAIL', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const resultTab = screen.getByRole('button', { name: 'runLog.result' }) const detailTab = screen.getByRole('button', { name: 'runLog.detail' }) const tracingTab = screen.getByRole('button', { name: 'runLog.tracing' }) @@ -771,11 +636,9 @@ describe('Tabs', () => { }) it('should set TRACING tab as active when currentTab is TRACING', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const resultTab = screen.getByRole('button', { name: 'runLog.result' }) const detailTab = screen.getByRole('button', { name: 'runLog.detail' }) const tracingTab = screen.getByRole('button', { name: 'runLog.tracing' }) @@ -795,11 +657,9 @@ describe('Tabs', () => { }) it('should handle unknown currentTab gracefully', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert - All tabs should be inactive const resultTab = screen.getByRole('button', { name: 'runLog.result' }) const detailTab = screen.getByRole('button', { name: 'runLog.detail' }) const tracingTab = screen.getByRole('button', { name: 'runLog.tracing' }) @@ -821,11 +680,9 @@ describe('Tabs', () => { describe('workflowRunningData prop', () => { it('should enable all tabs when workflowRunningData is provided', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const buttons = screen.getAllByRole('button') buttons.forEach((button) => { expect(button).not.toBeDisabled() @@ -842,10 +698,8 @@ describe('Tabs', () => { }) it('should disable all tabs when workflowRunningData is undefined', () => { - // Arrange const mockSwitchTab = vi.fn() - // Act render( { />, ) - // Assert const buttons = screen.getAllByRole('button') buttons.forEach((button) => { expect(button).toBeDisabled() @@ -863,11 +716,9 @@ describe('Tabs', () => { }) it('should pass workflowRunningData to all Tab components', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert - All tabs should be enabled (workflowRunningData passed) const buttons = screen.getAllByRole('button') buttons.forEach((button) => { expect(button).not.toHaveClass('opacity-30') @@ -886,11 +736,9 @@ describe('Tabs', () => { describe('switchTab prop', () => { it('should pass switchTab function to Tab onClick', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { ) fireEvent.click(screen.getByRole('button', { name: 'runLog.detail' })) - // Assert expect(mockSwitchTab).toHaveBeenCalledWith('DETAIL') }) }) }) - // ------------------------------------------------------------------------- - // Event Handlers Tests - Verify click behavior - // ------------------------------------------------------------------------- describe('Event Handlers', () => { it('should call switchTab with RESULT when RESULT tab is clicked', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { ) fireEvent.click(screen.getByRole('button', { name: 'runLog.result' })) - // Assert expect(mockSwitchTab).toHaveBeenCalledWith('RESULT') }) it('should call switchTab with DETAIL when DETAIL tab is clicked', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { ) fireEvent.click(screen.getByRole('button', { name: 'runLog.detail' })) - // Assert expect(mockSwitchTab).toHaveBeenCalledWith('DETAIL') }) it('should call switchTab with TRACING when TRACING tab is clicked', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { ) fireEvent.click(screen.getByRole('button', { name: 'runLog.tracing' })) - // Assert expect(mockSwitchTab).toHaveBeenCalledWith('TRACING') }) it('should not call switchTab when tabs are disabled', () => { - // Arrange const mockSwitchTab = vi.fn() - // Act render( { fireEvent.click(button) }) - // Assert expect(mockSwitchTab).not.toHaveBeenCalled() }) it('should allow clicking the currently active tab', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { ) fireEvent.click(screen.getByRole('button', { name: 'runLog.result' })) - // Assert expect(mockSwitchTab).toHaveBeenCalledWith('RESULT') }) }) - // ------------------------------------------------------------------------- - // Memoization Tests - Verify React.memo optimization - // ------------------------------------------------------------------------- describe('Memoization', () => { it('should not re-render when props are the same', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() const renderSpy = vi.fn() @@ -1025,7 +850,6 @@ describe('Tabs', () => { } const MemoizedTabsWithSpy = React.memo(TabsWithSpy) - // Act const { rerender } = render( { />, ) - // Re-render with same props rerender( { />, ) - // Assert - React.memo should prevent re-render with same props expect(renderSpy).toHaveBeenCalledTimes(1) }) it('should re-render when currentTab changes', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act const { rerender } = render( { />, ) - // Assert initial state expect(screen.getByRole('button', { name: 'runLog.result' })).toHaveClass('text-text-primary') - // Rerender with changed prop rerender( { />, ) - // Assert updated state expect(screen.getByRole('button', { name: 'runLog.result' })).toHaveClass('text-text-tertiary') expect(screen.getByRole('button', { name: 'runLog.detail' })).toHaveClass('text-text-primary') }) it('should re-render when workflowRunningData changes from undefined to defined', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act const { rerender } = render( { />, ) - // Assert initial disabled state const buttons = screen.getAllByRole('button') buttons.forEach((button) => { expect(button).toBeDisabled() }) - // Rerender with workflowRunningData rerender( { />, ) - // Assert enabled state const updatedButtons = screen.getAllByRole('button') updatedButtons.forEach((button) => { expect(button).not.toBeDisabled() @@ -1115,16 +927,11 @@ describe('Tabs', () => { }) }) - // ------------------------------------------------------------------------- - // Edge Cases Tests - Verify boundary conditions - // ------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle empty string currentTab', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert - All tabs should be inactive const buttons = screen.getAllByRole('button') buttons.forEach((button) => { expect(button).toHaveClass('text-text-tertiary') @@ -1141,11 +947,9 @@ describe('Tabs', () => { }) it('should handle case-sensitive tab values', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act - lowercase "result" should not match "RESULT" render( { />, ) - // Assert - Result tab should not be active (case mismatch) expect(screen.getByRole('button', { name: 'runLog.result' })).toHaveClass('text-text-tertiary') }) it('should handle whitespace in currentTab', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert - Should not match due to whitespace expect(screen.getByRole('button', { name: 'runLog.result' })).toHaveClass('text-text-tertiary') }) it('should render correctly with minimal workflowRunningData', () => { - // Arrange const mockSwitchTab = vi.fn() const minimalWorkflowData: WorkflowRunningData = { result: { @@ -1188,7 +987,6 @@ describe('Tabs', () => { }, } - // Act render( { />, ) - // Assert const buttons = screen.getAllByRole('button') buttons.forEach((button) => { expect(button).not.toBeDisabled() @@ -1205,11 +1002,9 @@ describe('Tabs', () => { }) it('should maintain tab order (RESULT, DETAIL, TRACING)', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert const buttons = screen.getAllByRole('button') expect(buttons[0]).toHaveTextContent('runLog.result') expect(buttons[1]).toHaveTextContent('runLog.detail') @@ -1226,16 +1020,11 @@ describe('Tabs', () => { }) }) - // ------------------------------------------------------------------------- - // Integration Tests - Verify Tab and Tabs work together - // ------------------------------------------------------------------------- describe('Integration', () => { it('should correctly pass all props to child Tab components', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act render( { />, ) - // Assert - Verify each tab has correct props const resultTab = screen.getByRole('button', { name: 'runLog.result' }) const detailTab = screen.getByRole('button', { name: 'runLog.detail' }) const tracingTab = screen.getByRole('button', { name: 'runLog.tracing' }) - // Check active states expect(resultTab).toHaveClass('text-text-tertiary') expect(detailTab).toHaveClass('text-text-primary') expect(tracingTab).toHaveClass('text-text-tertiary') - // Check enabled states expect(resultTab).not.toBeDisabled() expect(detailTab).not.toBeDisabled() expect(tracingTab).not.toBeDisabled() - // Check click handlers fireEvent.click(resultTab) expect(mockSwitchTab).toHaveBeenCalledWith('RESULT') @@ -1268,12 +1053,10 @@ describe('Tabs', () => { }) it('should support full tab switching workflow', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() let currentTab = 'RESULT' - // Act const { rerender } = render( { />, ) - // Simulate clicking DETAIL tab fireEvent.click(screen.getByRole('button', { name: 'runLog.detail' })) expect(mockSwitchTab).toHaveBeenCalledWith('DETAIL') - // Update currentTab and rerender (simulating parent state update) currentTab = 'DETAIL' rerender( { />, ) - // Assert DETAIL is now active expect(screen.getByRole('button', { name: 'runLog.detail' })).toHaveClass('text-text-primary') - // Simulate clicking TRACING tab fireEvent.click(screen.getByRole('button', { name: 'runLog.tracing' })) expect(mockSwitchTab).toHaveBeenCalledWith('TRACING') - // Update currentTab and rerender currentTab = 'TRACING' rerender( { />, ) - // Assert TRACING is now active expect(screen.getByRole('button', { name: 'runLog.tracing' })).toHaveClass('text-text-primary') }) it('should transition from disabled to enabled state', () => { - // Arrange const mockSwitchTab = vi.fn() const workflowData = createWorkflowRunningData() - // Act - Initial disabled state const { rerender } = render( { />, ) - // Try clicking - should not trigger fireEvent.click(screen.getByRole('button', { name: 'runLog.detail' })) expect(mockSwitchTab).not.toHaveBeenCalled() - // Enable tabs rerender( { />, ) - // Now click should work fireEvent.click(screen.getByRole('button', { name: 'runLog.detail' })) expect(mockSwitchTab).toHaveBeenCalledWith('DETAIL') }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/index.spec.tsx similarity index 84% rename from web/app/components/rag-pipeline/components/rag-pipeline-header/index.spec.tsx rename to web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/index.spec.tsx index 4f3465a920..0b858eaaa7 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/index.spec.tsx @@ -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 }>) => ( {children} ), })) -// 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() @@ -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() let viewHistoryContent = screen.getByTestId('header-view-history').textContent expect(viewHistoryContent).toContain('pipeline-alpha') unmount() - // Test with different pipelineId mockStoreState.pipelineId = 'pipeline-beta' render() 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() @@ -337,9 +299,6 @@ describe('InputFieldButton', () => { }) }) - // -------------------------------------------------------------------------- - // Event Handler Tests - // -------------------------------------------------------------------------- describe('Event Handlers', () => { it('should call setShowInputFieldPanel(true) when clicked', () => { render() @@ -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() - // 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() @@ -410,9 +362,6 @@ describe('Publisher', () => { }) }) - // -------------------------------------------------------------------------- - // Interaction Tests - // -------------------------------------------------------------------------- describe('Interactions', () => { it('should call handleSyncWorkflowDraft when opening', () => { render() @@ -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() @@ -475,7 +420,6 @@ describe('Popup', () => { it('should render keyboard shortcuts', () => { render() - // 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() @@ -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() @@ -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() - // 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() - // 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() @@ -838,7 +756,6 @@ describe('RunMode', () => { render() - // Click the stop button (second button) const buttons = screen.getAllByRole('button') fireEvent.click(buttons[1]) @@ -850,7 +767,6 @@ describe('RunMode', () => { render() - // 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() @@ -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() - // 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() - // 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()).not.toThrow() }) @@ -951,14 +859,10 @@ describe('RunMode', () => { render() - // useSubscription should not be called expect(mockEventEmitter.useSubscription).not.toHaveBeenCalled() }) }) - // -------------------------------------------------------------------------- - // Style Tests - // -------------------------------------------------------------------------- describe('Styles', () => { it('should have rounded-md class when not disabled', () => { render() @@ -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() - // 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()).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() - // Button should be disabled const runButton = screen.getAllByRole('button')[0] expect(runButton).toBeDisabled() }) @@ -1169,7 +1056,6 @@ describe('Edge Cases', () => { render() - // 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() - // 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() - // Verify keyboard shortcut elements exist expect(screen.getByText('alt')).toBeInTheDocument() expect(screen.getByText('R')).toBeInTheDocument() }) @@ -1216,7 +1100,6 @@ describe('Edge Cases', () => { render() - // 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() - // 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() - // Should render without crashing expect(screen.getByText(/workflow.common.autoSaved/i)).toBeInTheDocument() }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/run-mode.spec.tsx similarity index 90% rename from web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.spec.tsx rename to web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/run-mode.spec.tsx index 5448b30587..9ac47aae02 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/run-mode.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/__tests__/run-mode.spec.tsx @@ -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() - 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() - 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() - 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() - 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() - 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() - expect(screen.getByText('common.preparingDataSource')).toBeInTheDocument() + expect(screen.getByText('pipeline.common.preparingDataSource')).toBeInTheDocument() }) it('should show database icon', () => { diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx similarity index 81% rename from web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.spec.tsx rename to web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx index 2a01218ee6..0fc3bda7b3 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/index.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/index.spec.tsx @@ -3,29 +3,21 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import Publisher from './index' -import Popup from './popup' +import Publisher from '../index' +import Popup from '../popup' -// ================================ -// Mock External Dependencies Only -// ================================ - -// 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 }: { children: React.ReactNode, href: string }) => ( {children} ), })) -// Mock ahooks -// Store the keyboard shortcut callback for testing let keyPressCallback: ((e: KeyboardEvent) => void) | null = null vi.mock('ahooks', () => ({ useBoolean: (defaultValue = false) => { @@ -37,17 +29,14 @@ vi.mock('ahooks', () => ({ }] }, useKeyPress: (key: string, callback: (e: KeyboardEvent) => void) => { - // Store the callback so we can invoke it in tests keyPressCallback = callback }, })) -// Mock amplitude tracking vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn(), })) -// Mock portal-to-follow-elem let mockPortalOpen = false vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ PortalToFollowElem: ({ children, open, onOpenChange: _onOpenChange }: { @@ -76,7 +65,6 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({ }, })) -// Mock workflow hooks const mockHandleSyncWorkflowDraft = vi.fn() const mockHandleCheckBeforePublish = vi.fn().mockResolvedValue(true) vi.mock('@/app/components/workflow/hooks', () => ({ @@ -88,7 +76,6 @@ vi.mock('@/app/components/workflow/hooks', () => ({ }), })) -// Mock workflow store const mockPublishedAt = vi.fn(() => null as number | null) const mockDraftUpdatedAt = vi.fn(() => 1700000000) const mockPipelineId = vi.fn(() => 'test-pipeline-id') @@ -110,7 +97,6 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) -// Mock dataset-detail context const mockMutateDatasetRes = vi.fn() vi.mock('@/context/dataset-detail', () => ({ useDatasetDetailContextWithSelector: (selector: (s: Record) => unknown) => { @@ -119,13 +105,11 @@ vi.mock('@/context/dataset-detail', () => ({ }, })) -// Mock modal-context const mockSetShowPricingModal = vi.fn() vi.mock('@/context/modal-context', () => ({ useModalContextSelector: () => mockSetShowPricingModal, })) -// Mock provider-context const mockIsAllowPublishAsCustomKnowledgePipelineTemplate = vi.fn(() => true) vi.mock('@/context/provider-context', () => ({ useProviderContext: () => ({ @@ -135,7 +119,6 @@ vi.mock('@/context/provider-context', () => ({ selector({ isAllowPublishAsCustomKnowledgePipelineTemplate: mockIsAllowPublishAsCustomKnowledgePipelineTemplate() }), })) -// Mock toast context const mockNotify = vi.fn() vi.mock('@/app/components/base/toast', () => ({ useToastContext: () => ({ @@ -143,12 +126,10 @@ vi.mock('@/app/components/base/toast', () => ({ }), })) -// Mock API access URL hook vi.mock('@/hooks/use-api-access-url', () => ({ useDatasetApiAccessUrl: () => 'https://api.dify.ai/v1/datasets/test-dataset-id', })) -// Mock format time hook vi.mock('@/hooks/use-format-time-from-now', () => ({ useFormatTimeFromNow: () => ({ formatTimeFromNow: (timestamp: number) => { @@ -162,7 +143,6 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ }), })) -// Mock service hooks const mockPublishWorkflow = vi.fn() const mockPublishAsCustomizedPipeline = vi.fn() const mockInvalidPublishedPipelineInfo = vi.fn() @@ -191,14 +171,12 @@ vi.mock('@/service/use-workflow', () => ({ }), })) -// Mock workflow utils vi.mock('@/app/components/workflow/utils', () => ({ getKeyboardKeyCodeBySystem: (key: string) => key, getKeyboardKeyNameBySystem: (key: string) => key === 'ctrl' ? '⌘' : key, })) -// Mock PublishAsKnowledgePipelineModal -vi.mock('../../publish-as-knowledge-pipeline-modal', () => ({ +vi.mock('../../../publish-as-knowledge-pipeline-modal', () => ({ default: ({ confirmDisabled, onConfirm, onCancel }: { confirmDisabled: boolean onConfirm: (name: string, icon: IconInfo, description?: string) => void @@ -217,10 +195,6 @@ vi.mock('../../publish-as-knowledge-pipeline-modal', () => ({ ), })) -// ================================ -// Test Data Factories -// ================================ - const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { @@ -238,16 +212,11 @@ const renderWithQueryClient = (ui: React.ReactElement) => { ) } -// ================================ -// Test Suites -// ================================ - describe('publisher', () => { beforeEach(() => { vi.clearAllMocks() mockPortalOpen = false keyPressCallback = null - // Reset mock return values to defaults mockPublishedAt.mockReturnValue(null) mockDraftUpdatedAt.mockReturnValue(1700000000) mockPipelineId.mockReturnValue('test-pipeline-id') @@ -255,127 +224,90 @@ describe('publisher', () => { mockHandleCheckBeforePublish.mockResolvedValue(true) }) - // ============================================================ - // Publisher (index.tsx) - Main Entry Component Tests - // ============================================================ describe('Publisher (index.tsx)', () => { - // -------------------------------- - // Rendering Tests - // -------------------------------- describe('Rendering', () => { it('should render publish button with correct text', () => { - // Arrange & Act renderWithQueryClient() - // Assert expect(screen.getByRole('button')).toBeInTheDocument() expect(screen.getByText('workflow.common.publish')).toBeInTheDocument() }) it('should render portal element in closed state by default', () => { - // Arrange & Act renderWithQueryClient() - // Assert expect(screen.getByTestId('portal-elem')).toHaveAttribute('data-open', 'false') expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() }) it('should render down arrow icon in button', () => { - // Arrange & Act renderWithQueryClient() - // Assert const button = screen.getByRole('button') expect(button.querySelector('svg')).toBeInTheDocument() }) }) - // -------------------------------- - // State Management Tests - // -------------------------------- describe('State Management', () => { it('should open popup when trigger is clicked', async () => { - // Arrange renderWithQueryClient() - // Act fireEvent.click(screen.getByTestId('portal-trigger')) - // Assert await waitFor(() => { expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) }) it('should close popup when trigger is clicked again while open', async () => { - // Arrange renderWithQueryClient() fireEvent.click(screen.getByTestId('portal-trigger')) // open - // Act await waitFor(() => { expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('portal-trigger')) // close - // Assert await waitFor(() => { expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() }) }) }) - // -------------------------------- - // Callback Stability and Memoization Tests - // -------------------------------- describe('Callback Stability and Memoization', () => { it('should call handleSyncWorkflowDraft when popup opens', async () => { - // Arrange renderWithQueryClient() - // Act fireEvent.click(screen.getByTestId('portal-trigger')) - // Assert expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true) }) it('should not call handleSyncWorkflowDraft when popup closes', async () => { - // Arrange renderWithQueryClient() fireEvent.click(screen.getByTestId('portal-trigger')) // open vi.clearAllMocks() - // Act await waitFor(() => { expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) fireEvent.click(screen.getByTestId('portal-trigger')) // close - // Assert expect(mockHandleSyncWorkflowDraft).not.toHaveBeenCalled() }) it('should be memoized with React.memo', () => { - // Assert expect(Publisher).toBeDefined() expect((Publisher as unknown as { $$typeof?: symbol }).$$typeof?.toString()).toContain('Symbol') }) }) - // -------------------------------- - // User Interactions Tests - // -------------------------------- describe('User Interactions', () => { it('should render popup content when opened', async () => { - // Arrange renderWithQueryClient() - // Act fireEvent.click(screen.getByTestId('portal-trigger')) - // Assert await waitFor(() => { expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) @@ -383,68 +315,48 @@ describe('publisher', () => { }) }) - // ============================================================ - // Popup (popup.tsx) - Main Popup Component Tests - // ============================================================ describe('Popup (popup.tsx)', () => { - // -------------------------------- - // Rendering Tests - // -------------------------------- describe('Rendering', () => { it('should render unpublished state when publishedAt is null', () => { - // Arrange mockPublishedAt.mockReturnValue(null) - // Act renderWithQueryClient() - // Assert expect(screen.getByText('workflow.common.currentDraftUnpublished')).toBeInTheDocument() expect(screen.getByText(/workflow.common.autoSaved/)).toBeInTheDocument() }) it('should render published state when publishedAt has value', () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) - // Act renderWithQueryClient() - // Assert expect(screen.getByText('workflow.common.latestPublished')).toBeInTheDocument() expect(screen.getByText(/workflow.common.publishedAt/)).toBeInTheDocument() }) it('should render publish button with keyboard shortcuts', () => { - // Arrange & Act renderWithQueryClient() - // Assert const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) expect(publishButton).toBeInTheDocument() }) it('should render action buttons section', () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) - // Act renderWithQueryClient() - // Assert expect(screen.getByText('pipeline.common.goToAddDocuments')).toBeInTheDocument() expect(screen.getByText('workflow.common.accessAPIReference')).toBeInTheDocument() expect(screen.getByText('pipeline.common.publishAs')).toBeInTheDocument() }) it('should disable action buttons when not published', () => { - // Arrange mockPublishedAt.mockReturnValue(null) - // Act renderWithQueryClient() - // Assert const addDocumentsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.goToAddDocuments'), ) @@ -452,13 +364,10 @@ describe('publisher', () => { }) it('should enable action buttons when published', () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) - // Act renderWithQueryClient() - // Assert const addDocumentsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.goToAddDocuments'), ) @@ -466,137 +375,106 @@ describe('publisher', () => { }) it('should show premium badge when publish as template is not allowed', () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false) - // Act renderWithQueryClient() - // Assert expect(screen.getByText('billing.upgradeBtn.encourageShort')).toBeInTheDocument() }) it('should not show premium badge when publish as template is allowed', () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) - // Act renderWithQueryClient() - // Assert expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() }) }) - // -------------------------------- - // State Management Tests - // -------------------------------- describe('State Management', () => { it('should show confirm modal when first publish attempt on unpublished pipeline', async () => { - // Arrange mockPublishedAt.mockReturnValue(null) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert await waitFor(() => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) }) it('should not show confirm modal when already published', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert - should call publish directly without confirm await waitFor(() => { expect(mockPublishWorkflow).toHaveBeenCalled() }) }) it('should update to published state after successful publish', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert await waitFor(() => { expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeInTheDocument() }) }) }) - // -------------------------------- - // User Interactions Tests - // -------------------------------- describe('User Interactions', () => { it('should navigate to add documents when go to add documents is clicked', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) renderWithQueryClient() - // Act const addDocumentsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.goToAddDocuments'), ) fireEvent.click(addDocumentsButton!) - // Assert expect(mockPush).toHaveBeenCalledWith('/datasets/test-dataset-id/documents/create-from-pipeline') }) it('should show pricing modal when publish as template is clicked without permission', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false) renderWithQueryClient() - // Act const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), ) fireEvent.click(publishAsButton!) - // Assert expect(mockSetShowPricingModal).toHaveBeenCalled() }) it('should show publish as knowledge pipeline modal when permitted', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) renderWithQueryClient() - // Act const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), ) fireEvent.click(publishAsButton!) - // Assert await waitFor(() => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) }) it('should close publish as knowledge pipeline modal when cancel is clicked', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) renderWithQueryClient() @@ -610,17 +488,14 @@ describe('publisher', () => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) - // Act fireEvent.click(screen.getByTestId('modal-cancel')) - // Assert await waitFor(() => { expect(screen.queryByTestId('publish-as-knowledge-pipeline-modal')).not.toBeInTheDocument() }) }) it('should call publishAsCustomizedPipeline when confirm is clicked in modal', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) renderWithQueryClient() @@ -634,10 +509,8 @@ describe('publisher', () => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) - // Act fireEvent.click(screen.getByTestId('modal-confirm')) - // Assert await waitFor(() => { expect(mockPublishAsCustomizedPipeline).toHaveBeenCalledWith({ pipelineId: 'test-pipeline-id', @@ -649,21 +522,15 @@ describe('publisher', () => { }) }) - // -------------------------------- - // API Calls and Async Operations Tests - // -------------------------------- describe('API Calls and Async Operations', () => { it('should call publishWorkflow API when publish button is clicked', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert await waitFor(() => { expect(mockPublishWorkflow).toHaveBeenCalledWith({ url: '/rag/pipelines/test-pipeline-id/workflows/publish', @@ -674,16 +541,13 @@ describe('publisher', () => { }) it('should show success notification after publish', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith( expect.objectContaining({ @@ -695,32 +559,26 @@ describe('publisher', () => { }) it('should update publishedAt in store after successful publish', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert await waitFor(() => { expect(mockSetPublishedAt).toHaveBeenCalledWith(1700100000) }) }) it('should invalidate caches after successful publish', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert await waitFor(() => { expect(mockMutateDatasetRes).toHaveBeenCalled() expect(mockInvalidPublishedPipelineInfo).toHaveBeenCalled() @@ -729,7 +587,6 @@ describe('publisher', () => { }) it('should show success notification for publish as template', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) renderWithQueryClient() @@ -743,10 +600,8 @@ describe('publisher', () => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) - // Act fireEvent.click(screen.getByTestId('modal-confirm')) - // Assert await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith( expect.objectContaining({ @@ -758,7 +613,6 @@ describe('publisher', () => { }) it('should invalidate customized template list after publish as template', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) renderWithQueryClient() @@ -772,31 +626,23 @@ describe('publisher', () => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) - // Act fireEvent.click(screen.getByTestId('modal-confirm')) - // Assert await waitFor(() => { expect(mockInvalidCustomizedTemplateList).toHaveBeenCalled() }) }) }) - // -------------------------------- - // Error Handling Tests - // -------------------------------- describe('Error Handling', () => { it('should not proceed with publish when check fails', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockHandleCheckBeforePublish.mockResolvedValue(false) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert - publishWorkflow should not be called when check fails await waitFor(() => { expect(mockHandleCheckBeforePublish).toHaveBeenCalled() }) @@ -804,16 +650,13 @@ describe('publisher', () => { }) it('should show error notification when publish fails', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockRejectedValue(new Error('Publish failed')) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', @@ -823,7 +666,6 @@ describe('publisher', () => { }) it('should show error notification when publish as template fails', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockRejectedValue(new Error('Template publish failed')) renderWithQueryClient() @@ -837,10 +679,8 @@ describe('publisher', () => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) - // Act fireEvent.click(screen.getByTestId('modal-confirm')) - // Assert await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', @@ -850,7 +690,6 @@ describe('publisher', () => { }) it('should close modal after publish as template error', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockRejectedValue(new Error('Template publish failed')) renderWithQueryClient() @@ -864,22 +703,16 @@ describe('publisher', () => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) - // Act fireEvent.click(screen.getByTestId('modal-confirm')) - // Assert await waitFor(() => { expect(screen.queryByTestId('publish-as-knowledge-pipeline-modal')).not.toBeInTheDocument() }) }) }) - // -------------------------------- - // Confirm Modal Tests - // -------------------------------- describe('Confirm Modal', () => { it('should hide confirm modal when cancel is clicked', async () => { - // Arrange mockPublishedAt.mockReturnValue(null) renderWithQueryClient() @@ -890,7 +723,6 @@ describe('publisher', () => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) - // Act - find and click cancel button in confirm modal const cancelButtons = screen.getAllByRole('button') const cancelButton = cancelButtons.find(btn => btn.className.includes('cancel') || btn.textContent?.includes('Cancel'), @@ -898,16 +730,11 @@ describe('publisher', () => { if (cancelButton) fireEvent.click(cancelButton) - // Trigger onCancel manually since we can't find the exact button - // The Confirm component has an onCancel prop that calls hideConfirm - - // Assert - modal should be dismissable // Note: This test verifies the confirm modal can be displayed expect(screen.getByText('pipeline.common.confirmPublishContent')).toBeInTheDocument() }) it('should publish when confirm is clicked in confirm modal', async () => { - // Arrange mockPublishedAt.mockReturnValue(null) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() @@ -919,28 +746,19 @@ describe('publisher', () => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) - // Assert - confirm modal content is displayed expect(screen.getByText('pipeline.common.confirmPublishContent')).toBeInTheDocument() }) }) - // -------------------------------- - // Component Memoization Tests - // -------------------------------- describe('Component Memoization', () => { it('should be memoized with React.memo', () => { - // Assert expect(Popup).toBeDefined() expect((Popup as unknown as { $$typeof?: symbol }).$$typeof?.toString()).toContain('Symbol') }) }) - // -------------------------------- - // Prop Variations Tests - // -------------------------------- describe('Prop Variations', () => { it('should display correct width when permission is allowed', () => { - // Test with permission mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(true) const { container } = renderWithQueryClient() @@ -949,7 +767,6 @@ describe('publisher', () => { }) it('should display correct width when permission is not allowed', () => { - // Test without permission mockIsAllowPublishAsCustomKnowledgePipelineTemplate.mockReturnValue(false) const { container } = renderWithQueryClient() @@ -958,63 +775,45 @@ describe('publisher', () => { }) it('should display draft updated time when not published', () => { - // Arrange mockPublishedAt.mockReturnValue(null) mockDraftUpdatedAt.mockReturnValue(1700000000) - // Act renderWithQueryClient() - // Assert expect(screen.getByText(/workflow.common.autoSaved/)).toBeInTheDocument() }) it('should handle null draftUpdatedAt gracefully', () => { - // Arrange mockPublishedAt.mockReturnValue(null) mockDraftUpdatedAt.mockReturnValue(0) - // Act renderWithQueryClient() - // Assert expect(screen.getByText(/workflow.common.autoSaved/)).toBeInTheDocument() }) }) - // -------------------------------- - // API Reference Link Tests - // -------------------------------- describe('API Reference Link', () => { it('should render API reference link with correct href', () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) - // Act renderWithQueryClient() - // Assert const apiLink = screen.getByRole('link') expect(apiLink).toHaveAttribute('href', 'https://api.dify.ai/v1/datasets/test-dataset-id') expect(apiLink).toHaveAttribute('target', '_blank') }) }) - // -------------------------------- - // Keyboard Shortcut Tests - // -------------------------------- describe('Keyboard Shortcuts', () => { it('should trigger publish when keyboard shortcut is pressed', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act - simulate keyboard shortcut const mockEvent = { preventDefault: vi.fn() } as unknown as KeyboardEvent keyPressCallback?.(mockEvent) - // Assert expect(mockEvent.preventDefault).toHaveBeenCalled() await waitFor(() => { expect(mockPublishWorkflow).toHaveBeenCalled() @@ -1022,12 +821,10 @@ describe('publisher', () => { }) it('should not trigger publish when already published in session', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // First publish via button click to set published state const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) @@ -1037,32 +834,26 @@ describe('publisher', () => { vi.clearAllMocks() - // Act - simulate keyboard shortcut after already published const mockEvent = { preventDefault: vi.fn() } as unknown as KeyboardEvent keyPressCallback?.(mockEvent) - // Assert - should return early without publishing expect(mockEvent.preventDefault).toHaveBeenCalled() expect(mockPublishWorkflow).not.toHaveBeenCalled() }) it('should show confirm modal when shortcut pressed on unpublished pipeline', async () => { - // Arrange mockPublishedAt.mockReturnValue(null) renderWithQueryClient() - // Act - simulate keyboard shortcut const mockEvent = { preventDefault: vi.fn() } as unknown as KeyboardEvent keyPressCallback?.(mockEvent) - // Assert await waitFor(() => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) }) it('should not trigger duplicate publish via shortcut when already publishing', async () => { - // Arrange - create a promise that we can control let resolvePublish: () => void = () => {} mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockImplementation(() => new Promise((resolve) => { @@ -1070,59 +861,45 @@ describe('publisher', () => { })) renderWithQueryClient() - // Act - trigger publish via keyboard shortcut first const mockEvent1 = { preventDefault: vi.fn() } as unknown as KeyboardEvent keyPressCallback?.(mockEvent1) - // Wait for the first publish to start (button becomes disabled) await waitFor(() => { const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) expect(publishButton).toBeDisabled() }) - // Try to trigger again via shortcut while publishing const mockEvent2 = { preventDefault: vi.fn() } as unknown as KeyboardEvent keyPressCallback?.(mockEvent2) - // Assert - only one call to publishWorkflow expect(mockPublishWorkflow).toHaveBeenCalledTimes(1) - // Cleanup - resolve the promise resolvePublish() }) }) - // -------------------------------- - // Finally Block Cleanup Tests - // -------------------------------- describe('Finally Block Cleanup', () => { it('should reset publishing state after successful publish', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert - button should be disabled during publishing, then show published await waitFor(() => { expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeInTheDocument() }) }) it('should reset publishing state after failed publish', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockRejectedValue(new Error('Publish failed')) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert - should show error and button should be enabled again (not showing "published") await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', @@ -1130,19 +907,16 @@ describe('publisher', () => { }) }) - // Button should still show publishUpdate since it wasn't successfully published await waitFor(() => { expect(screen.getByRole('button', { name: /workflow.common.publishUpdate/i })).toBeInTheDocument() }) }) it('should hide confirm modal after publish from confirm', async () => { - // Arrange mockPublishedAt.mockReturnValue(null) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Show confirm modal first const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) @@ -1150,25 +924,18 @@ describe('publisher', () => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) - // Act - trigger publish again (which happens when confirm is clicked) - // The mock for workflow hooks returns handleCheckBeforePublish that resolves to true - // We need to simulate the confirm button click which calls handlePublish again - // Since confirmVisible is now true and publishedAt is null, it should proceed to publish fireEvent.click(publishButton) - // Assert - confirm modal should be hidden after publish completes await waitFor(() => { expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeInTheDocument() }) }) it('should hide confirm modal after failed publish', async () => { - // Arrange mockPublishedAt.mockReturnValue(null) mockPublishWorkflow.mockRejectedValue(new Error('Publish failed')) renderWithQueryClient() - // Show confirm modal first const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) @@ -1176,10 +943,8 @@ describe('publisher', () => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) - // Act - trigger publish from confirm (call handlePublish when confirmVisible is true) fireEvent.click(publishButton) - // Assert - error notification should be shown await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', @@ -1190,137 +955,104 @@ describe('publisher', () => { }) }) - // ============================================================ - // Edge Cases - // ============================================================ describe('Edge Cases', () => { it('should handle undefined pipelineId gracefully', () => { - // Arrange mockPipelineId.mockReturnValue('') - // Act renderWithQueryClient() - // Assert - should render without crashing expect(screen.getByText('workflow.common.currentDraftUnpublished')).toBeInTheDocument() }) it('should handle empty publish response', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue(null) renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert - should not call setPublishedAt or notify when response is null await waitFor(() => { expect(mockPublishWorkflow).toHaveBeenCalled() }) - // setPublishedAt should not be called because res is falsy expect(mockSetPublishedAt).not.toHaveBeenCalled() }) it('should prevent multiple simultaneous publish calls', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) - // Create a promise that never resolves to simulate ongoing publish mockPublishWorkflow.mockImplementation(() => new Promise(() => {})) renderWithQueryClient() - // Act - click publish button multiple times rapidly const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Wait for button to become disabled await waitFor(() => { expect(publishButton).toBeDisabled() }) - // Try clicking again fireEvent.click(publishButton) fireEvent.click(publishButton) - // Assert - publishWorkflow should only be called once due to guard expect(mockPublishWorkflow).toHaveBeenCalledTimes(1) }) it('should disable publish button when already published in session', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act - publish once const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert - button should show "published" state await waitFor(() => { expect(screen.getByRole('button', { name: /workflow.common.published/i })).toBeDisabled() }) }) it('should not trigger publish when already publishing', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishWorkflow.mockImplementation(() => new Promise(() => {})) // Never resolves renderWithQueryClient() - // Act const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // The button should be disabled while publishing await waitFor(() => { expect(publishButton).toBeDisabled() }) }) }) - // ============================================================ - // Integration Tests - // ============================================================ describe('Integration Tests', () => { it('should complete full publish flow for unpublished pipeline', async () => { - // Arrange mockPublishedAt.mockReturnValue(null) mockPublishWorkflow.mockResolvedValue({ created_at: 1700100000 }) renderWithQueryClient() - // Act - click publish to show confirm const publishButton = screen.getByRole('button', { name: /workflow.common.publishUpdate/i }) fireEvent.click(publishButton) - // Assert - confirm modal should appear await waitFor(() => { expect(screen.getByText('pipeline.common.confirmPublish')).toBeInTheDocument() }) }) it('should complete full publish as template flow', async () => { - // Arrange mockPublishedAt.mockReturnValue(1700000000) mockPublishAsCustomizedPipeline.mockResolvedValue({}) renderWithQueryClient() - // Act - click publish as template button const publishAsButton = screen.getAllByRole('button').find(btn => btn.textContent?.includes('pipeline.common.publishAs'), ) fireEvent.click(publishAsButton!) - // Assert - modal should appear await waitFor(() => { expect(screen.getByTestId('publish-as-knowledge-pipeline-modal')).toBeInTheDocument() }) - // Act - confirm fireEvent.click(screen.getByTestId('modal-confirm')) - // Assert - success notification and modal closes await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith( expect.objectContaining({ @@ -1332,18 +1064,14 @@ describe('publisher', () => { }) it('should show Publisher button and open popup with Popup component', async () => { - // Arrange & Act renderWithQueryClient() - // Click to open popup fireEvent.click(screen.getByTestId('portal-trigger')) - // Assert await waitFor(() => { expect(screen.getByTestId('portal-content')).toBeInTheDocument() }) - // Verify sync was called when opening expect(mockHandleSyncWorkflowDraft).toHaveBeenCalledWith(true) }) }) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.spec.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx similarity index 87% rename from web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.spec.tsx rename to web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx index 10ae089e6e..71707721a4 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/popup.spec.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/publisher/__tests__/popup.spec.tsx @@ -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 }) => {i18nKey}, -})) - 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 }) => (