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