mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 22:28:46 +00:00
787 lines
25 KiB
TypeScript
787 lines
25 KiB
TypeScript
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
import PipelineSettings from './index'
|
|
import { DatasourceType } from '@/models/pipeline'
|
|
import type { PipelineExecutionLogResponse } from '@/models/pipeline'
|
|
|
|
// Mock i18n
|
|
jest.mock('react-i18next', () => ({
|
|
useTranslation: () => ({
|
|
t: (key: string) => key,
|
|
}),
|
|
}))
|
|
|
|
// Mock Next.js router
|
|
const mockPush = jest.fn()
|
|
const mockBack = jest.fn()
|
|
jest.mock('next/navigation', () => ({
|
|
useRouter: () => ({
|
|
push: mockPush,
|
|
back: mockBack,
|
|
}),
|
|
}))
|
|
|
|
// Mock dataset detail context
|
|
const mockPipelineId = 'pipeline-123'
|
|
jest.mock('@/context/dataset-detail', () => ({
|
|
useDatasetDetailContextWithSelector: (selector: (state: { dataset: { pipeline_id: string; doc_form: string } }) => unknown) =>
|
|
selector({ dataset: { pipeline_id: mockPipelineId, doc_form: 'text_model' } }),
|
|
}))
|
|
|
|
// Mock API hooks for PipelineSettings
|
|
const mockUsePipelineExecutionLog = jest.fn()
|
|
const mockMutateAsync = jest.fn()
|
|
const mockUseRunPublishedPipeline = jest.fn()
|
|
jest.mock('@/service/use-pipeline', () => ({
|
|
usePipelineExecutionLog: (params: { dataset_id: string; document_id: string }) => mockUsePipelineExecutionLog(params),
|
|
useRunPublishedPipeline: () => mockUseRunPublishedPipeline(),
|
|
// For ProcessDocuments component
|
|
usePublishedPipelineProcessingParams: () => ({
|
|
data: { variables: [] },
|
|
isFetching: false,
|
|
}),
|
|
}))
|
|
|
|
// Mock document invalidation hooks
|
|
const mockInvalidDocumentList = jest.fn()
|
|
const mockInvalidDocumentDetail = jest.fn()
|
|
jest.mock('@/service/knowledge/use-document', () => ({
|
|
useInvalidDocumentList: () => mockInvalidDocumentList,
|
|
useInvalidDocumentDetail: () => mockInvalidDocumentDetail,
|
|
}))
|
|
|
|
// Mock Form component in ProcessDocuments - internal dependencies are too complex
|
|
jest.mock('../../../create-from-pipeline/process-documents/form', () => {
|
|
return function MockForm({
|
|
ref,
|
|
initialData,
|
|
configurations,
|
|
onSubmit,
|
|
onPreview,
|
|
isRunning,
|
|
}: {
|
|
ref: React.RefObject<{ submit: () => void }>
|
|
initialData: Record<string, unknown>
|
|
configurations: Array<{ variable: string; label: string; type: string }>
|
|
schema: unknown
|
|
onSubmit: (data: Record<string, unknown>) => void
|
|
onPreview: () => void
|
|
isRunning: boolean
|
|
}) {
|
|
if (ref && typeof ref === 'object' && 'current' in ref) {
|
|
(ref as React.MutableRefObject<{ submit: () => void }>).current = {
|
|
submit: () => onSubmit(initialData),
|
|
}
|
|
}
|
|
return (
|
|
<form
|
|
data-testid="process-form"
|
|
onSubmit={(e) => {
|
|
e.preventDefault()
|
|
onSubmit(initialData)
|
|
}}
|
|
>
|
|
{configurations.map((config, index) => (
|
|
<div key={index} data-testid={`field-${config.variable}`}>
|
|
<label>{config.label}</label>
|
|
</div>
|
|
))}
|
|
<button type="button" data-testid="preview-btn" onClick={onPreview} disabled={isRunning}>
|
|
Preview
|
|
</button>
|
|
</form>
|
|
)
|
|
}
|
|
})
|
|
|
|
// Mock ChunkPreview - has complex internal state and many dependencies
|
|
jest.mock('../../../create-from-pipeline/preview/chunk-preview', () => {
|
|
return function MockChunkPreview({
|
|
dataSourceType,
|
|
localFiles,
|
|
onlineDocuments,
|
|
websitePages,
|
|
onlineDriveFiles,
|
|
isIdle,
|
|
isPending,
|
|
estimateData,
|
|
}: {
|
|
dataSourceType: string
|
|
localFiles: unknown[]
|
|
onlineDocuments: unknown[]
|
|
websitePages: unknown[]
|
|
onlineDriveFiles: unknown[]
|
|
isIdle: boolean
|
|
isPending: boolean
|
|
estimateData: unknown
|
|
}) {
|
|
return (
|
|
<div data-testid="chunk-preview">
|
|
<span data-testid="datasource-type">{dataSourceType}</span>
|
|
<span data-testid="local-files-count">{localFiles.length}</span>
|
|
<span data-testid="online-documents-count">{onlineDocuments.length}</span>
|
|
<span data-testid="website-pages-count">{websitePages.length}</span>
|
|
<span data-testid="online-drive-files-count">{onlineDriveFiles.length}</span>
|
|
<span data-testid="is-idle">{String(isIdle)}</span>
|
|
<span data-testid="is-pending">{String(isPending)}</span>
|
|
<span data-testid="has-estimate-data">{String(!!estimateData)}</span>
|
|
</div>
|
|
)
|
|
}
|
|
})
|
|
|
|
// Test utilities
|
|
const createQueryClient = () =>
|
|
new QueryClient({
|
|
defaultOptions: {
|
|
queries: { retry: false },
|
|
mutations: { retry: false },
|
|
},
|
|
})
|
|
|
|
const renderWithProviders = (ui: React.ReactElement) => {
|
|
const queryClient = createQueryClient()
|
|
return render(
|
|
<QueryClientProvider client={queryClient}>
|
|
{ui}
|
|
</QueryClientProvider>,
|
|
)
|
|
}
|
|
|
|
// Factory functions for test data
|
|
const createMockExecutionLogResponse = (
|
|
overrides: Partial<PipelineExecutionLogResponse> = {},
|
|
): PipelineExecutionLogResponse => ({
|
|
datasource_type: DatasourceType.localFile,
|
|
input_data: { chunk_size: '100' },
|
|
datasource_node_id: 'datasource-node-1',
|
|
datasource_info: {
|
|
related_id: 'file-1',
|
|
name: 'test-file.pdf',
|
|
extension: 'pdf',
|
|
},
|
|
...overrides,
|
|
})
|
|
|
|
const createDefaultProps = () => ({
|
|
datasetId: 'dataset-123',
|
|
documentId: 'document-456',
|
|
})
|
|
|
|
describe('PipelineSettings', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
mockPush.mockClear()
|
|
mockBack.mockClear()
|
|
mockMutateAsync.mockClear()
|
|
mockInvalidDocumentList.mockClear()
|
|
mockInvalidDocumentDetail.mockClear()
|
|
|
|
// Default: successful data fetch
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: createMockExecutionLogResponse(),
|
|
isFetching: false,
|
|
isError: false,
|
|
})
|
|
|
|
// Default: useRunPublishedPipeline mock
|
|
mockUseRunPublishedPipeline.mockReturnValue({
|
|
mutateAsync: mockMutateAsync,
|
|
isIdle: true,
|
|
isPending: false,
|
|
})
|
|
})
|
|
|
|
// ==================== Rendering Tests ====================
|
|
// Test basic rendering with real components
|
|
describe('Rendering', () => {
|
|
it('should render without crashing when data is loaded', () => {
|
|
// Arrange
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert - Real LeftHeader should render with correct content
|
|
expect(screen.getByText('datasetPipeline.documentSettings.title')).toBeInTheDocument()
|
|
expect(screen.getByText('datasetPipeline.addDocuments.steps.processDocuments')).toBeInTheDocument()
|
|
// Real ProcessDocuments should render
|
|
expect(screen.getByTestId('process-form')).toBeInTheDocument()
|
|
// ChunkPreview should render
|
|
expect(screen.getByTestId('chunk-preview')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render Loading component when fetching data', () => {
|
|
// Arrange
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: undefined,
|
|
isFetching: true,
|
|
isError: false,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert - Loading component should be rendered, not main content
|
|
expect(screen.queryByText('datasetPipeline.documentSettings.title')).not.toBeInTheDocument()
|
|
expect(screen.queryByTestId('process-form')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render AppUnavailable when there is an error', () => {
|
|
// Arrange
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: undefined,
|
|
isFetching: false,
|
|
isError: true,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert - AppUnavailable should be rendered
|
|
expect(screen.queryByText('datasetPipeline.documentSettings.title')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should render container with correct CSS classes', () => {
|
|
// Arrange
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
const { container } = renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
const mainContainer = container.firstChild as HTMLElement
|
|
expect(mainContainer).toHaveClass('relative', 'flex', 'min-w-[1024px]')
|
|
})
|
|
})
|
|
|
|
// ==================== LeftHeader Integration ====================
|
|
// Test real LeftHeader component behavior
|
|
describe('LeftHeader Integration', () => {
|
|
it('should render LeftHeader with title prop', () => {
|
|
// Arrange
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert - LeftHeader displays the title
|
|
expect(screen.getByText('datasetPipeline.documentSettings.title')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render back button in LeftHeader', () => {
|
|
// Arrange
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert - Back button should exist with proper aria-label
|
|
const backButton = screen.getByRole('button', { name: 'common.operation.back' })
|
|
expect(backButton).toBeInTheDocument()
|
|
})
|
|
|
|
it('should call router.back when back button is clicked', () => {
|
|
// Arrange
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
const backButton = screen.getByRole('button', { name: 'common.operation.back' })
|
|
fireEvent.click(backButton)
|
|
|
|
// Assert
|
|
expect(mockBack).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
// ==================== Props Testing ====================
|
|
describe('Props', () => {
|
|
it('should pass datasetId and documentId to usePipelineExecutionLog', () => {
|
|
// Arrange
|
|
const props = { datasetId: 'custom-dataset', documentId: 'custom-document' }
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(mockUsePipelineExecutionLog).toHaveBeenCalledWith({
|
|
dataset_id: 'custom-dataset',
|
|
document_id: 'custom-document',
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== Memoization - Data Transformation ====================
|
|
describe('Memoization - Data Transformation', () => {
|
|
it('should transform localFile datasource correctly', () => {
|
|
// Arrange
|
|
const mockData = createMockExecutionLogResponse({
|
|
datasource_type: DatasourceType.localFile,
|
|
datasource_info: {
|
|
related_id: 'file-123',
|
|
name: 'document.pdf',
|
|
extension: 'pdf',
|
|
},
|
|
})
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: mockData,
|
|
isFetching: false,
|
|
isError: false,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('local-files-count')).toHaveTextContent('1')
|
|
expect(screen.getByTestId('datasource-type')).toHaveTextContent(DatasourceType.localFile)
|
|
})
|
|
|
|
it('should transform websiteCrawl datasource correctly', () => {
|
|
// Arrange
|
|
const mockData = createMockExecutionLogResponse({
|
|
datasource_type: DatasourceType.websiteCrawl,
|
|
datasource_info: {
|
|
content: 'Page content',
|
|
description: 'Page description',
|
|
source_url: 'https://example.com/page',
|
|
title: 'Page Title',
|
|
},
|
|
})
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: mockData,
|
|
isFetching: false,
|
|
isError: false,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('website-pages-count')).toHaveTextContent('1')
|
|
expect(screen.getByTestId('local-files-count')).toHaveTextContent('0')
|
|
})
|
|
|
|
it('should transform onlineDocument datasource correctly', () => {
|
|
// Arrange
|
|
const mockData = createMockExecutionLogResponse({
|
|
datasource_type: DatasourceType.onlineDocument,
|
|
datasource_info: {
|
|
workspace_id: 'workspace-1',
|
|
page: { page_id: 'page-1', page_name: 'Notion Page' },
|
|
},
|
|
})
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: mockData,
|
|
isFetching: false,
|
|
isError: false,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('online-documents-count')).toHaveTextContent('1')
|
|
})
|
|
|
|
it('should transform onlineDrive datasource correctly', () => {
|
|
// Arrange
|
|
const mockData = createMockExecutionLogResponse({
|
|
datasource_type: DatasourceType.onlineDrive,
|
|
datasource_info: { id: 'drive-1', type: 'doc', name: 'Google Doc', size: 1024 },
|
|
})
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: mockData,
|
|
isFetching: false,
|
|
isError: false,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('online-drive-files-count')).toHaveTextContent('1')
|
|
})
|
|
})
|
|
|
|
// ==================== User Interactions - Process ====================
|
|
describe('User Interactions - Process', () => {
|
|
it('should trigger form submit when process button is clicked', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockResolvedValue({})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
// Find the "Save and Process" button (from real ProcessDocuments > Actions)
|
|
const processButton = screen.getByRole('button', { name: 'datasetPipeline.operations.saveAndProcess' })
|
|
fireEvent.click(processButton)
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call handleProcess with is_preview=false', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockResolvedValue({})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.operations.saveAndProcess' }))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
is_preview: false,
|
|
pipeline_id: mockPipelineId,
|
|
original_document_id: 'document-456',
|
|
}),
|
|
expect.any(Object),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should navigate to documents list after successful process', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockImplementation((_request, options) => {
|
|
options?.onSuccess?.()
|
|
return Promise.resolve({})
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.operations.saveAndProcess' }))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockPush).toHaveBeenCalledWith('/datasets/dataset-123/documents')
|
|
})
|
|
})
|
|
|
|
it('should invalidate document cache after successful process', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockImplementation((_request, options) => {
|
|
options?.onSuccess?.()
|
|
return Promise.resolve({})
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.operations.saveAndProcess' }))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockInvalidDocumentList).toHaveBeenCalled()
|
|
expect(mockInvalidDocumentDetail).toHaveBeenCalled()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== User Interactions - Preview ====================
|
|
describe('User Interactions - Preview', () => {
|
|
it('should trigger preview when preview button is clicked', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockResolvedValue({ data: { outputs: {} } })
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByTestId('preview-btn'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('should call handlePreviewChunks with is_preview=true', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockResolvedValue({ data: { outputs: {} } })
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByTestId('preview-btn'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
is_preview: true,
|
|
pipeline_id: mockPipelineId,
|
|
}),
|
|
expect.any(Object),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should update estimateData on successful preview', async () => {
|
|
// Arrange
|
|
const mockOutputs = { chunks: [], total_tokens: 50 }
|
|
mockMutateAsync.mockImplementation((_req, opts) => {
|
|
opts?.onSuccess?.({ data: { outputs: mockOutputs } })
|
|
return Promise.resolve({ data: { outputs: mockOutputs } })
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByTestId('preview-btn'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('has-estimate-data')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== API Integration ====================
|
|
describe('API Integration', () => {
|
|
it('should pass correct parameters for preview', async () => {
|
|
// Arrange
|
|
const mockData = createMockExecutionLogResponse({
|
|
datasource_type: DatasourceType.localFile,
|
|
datasource_node_id: 'node-xyz',
|
|
datasource_info: { related_id: 'file-1', name: 'test.pdf', extension: 'pdf' },
|
|
input_data: {},
|
|
})
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: mockData,
|
|
isFetching: false,
|
|
isError: false,
|
|
})
|
|
mockMutateAsync.mockResolvedValue({ data: { outputs: {} } })
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByTestId('preview-btn'))
|
|
|
|
// Assert - inputs come from initialData which is transformed by useInitialData
|
|
// Since usePublishedPipelineProcessingParams returns empty variables, inputs is {}
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
{
|
|
pipeline_id: mockPipelineId,
|
|
inputs: {},
|
|
start_node_id: 'node-xyz',
|
|
datasource_type: DatasourceType.localFile,
|
|
datasource_info_list: [{ related_id: 'file-1', name: 'test.pdf', extension: 'pdf' }],
|
|
is_preview: true,
|
|
},
|
|
expect.any(Object),
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ==================== Edge Cases ====================
|
|
describe('Edge Cases', () => {
|
|
it.each([
|
|
[DatasourceType.localFile, 'local-files-count', '1'],
|
|
[DatasourceType.websiteCrawl, 'website-pages-count', '1'],
|
|
[DatasourceType.onlineDocument, 'online-documents-count', '1'],
|
|
[DatasourceType.onlineDrive, 'online-drive-files-count', '1'],
|
|
])('should handle %s datasource type correctly', (datasourceType, testId, expectedCount) => {
|
|
// Arrange
|
|
const datasourceInfoMap: Record<DatasourceType, Record<string, unknown>> = {
|
|
[DatasourceType.localFile]: { related_id: 'f1', name: 'file.pdf', extension: 'pdf' },
|
|
[DatasourceType.websiteCrawl]: { content: 'c', description: 'd', source_url: 'u', title: 't' },
|
|
[DatasourceType.onlineDocument]: { workspace_id: 'w1', page: { page_id: 'p1' } },
|
|
[DatasourceType.onlineDrive]: { id: 'd1', type: 'doc', name: 'n', size: 100 },
|
|
}
|
|
|
|
const mockData = createMockExecutionLogResponse({
|
|
datasource_type: datasourceType,
|
|
datasource_info: datasourceInfoMap[datasourceType],
|
|
})
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: mockData,
|
|
isFetching: false,
|
|
isError: false,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.getByTestId(testId)).toHaveTextContent(expectedCount)
|
|
})
|
|
|
|
it('should show loading state during initial fetch', () => {
|
|
// Arrange
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: undefined,
|
|
isFetching: true,
|
|
isError: false,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.queryByTestId('process-form')).not.toBeInTheDocument()
|
|
})
|
|
|
|
it('should show error state when API fails', () => {
|
|
// Arrange
|
|
mockUsePipelineExecutionLog.mockReturnValue({
|
|
data: undefined,
|
|
isFetching: false,
|
|
isError: true,
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.queryByTestId('process-form')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
// ==================== State Management ====================
|
|
describe('State Management', () => {
|
|
it('should initialize with undefined estimateData', () => {
|
|
// Arrange
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Assert
|
|
expect(screen.getByTestId('has-estimate-data')).toHaveTextContent('false')
|
|
})
|
|
|
|
it('should update estimateData after successful preview', async () => {
|
|
// Arrange
|
|
const mockEstimateData = { chunks: [], total_tokens: 50 }
|
|
mockMutateAsync.mockImplementation((_req, opts) => {
|
|
opts?.onSuccess?.({ data: { outputs: mockEstimateData } })
|
|
return Promise.resolve({ data: { outputs: mockEstimateData } })
|
|
})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByTestId('preview-btn'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('has-estimate-data')).toHaveTextContent('true')
|
|
})
|
|
})
|
|
|
|
it('should set isPreview ref to false when process is clicked', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockResolvedValue({})
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.operations.saveAndProcess' }))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({ is_preview: false }),
|
|
expect.any(Object),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should set isPreview ref to true when preview is clicked', async () => {
|
|
// Arrange
|
|
mockMutateAsync.mockResolvedValue({ data: { outputs: {} } })
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
fireEvent.click(screen.getByTestId('preview-btn'))
|
|
|
|
// Assert
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({ is_preview: true }),
|
|
expect.any(Object),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should pass isPending=true to ChunkPreview when preview is pending', async () => {
|
|
// Arrange - Start with isPending=false so buttons are enabled
|
|
let isPendingState = false
|
|
mockUseRunPublishedPipeline.mockImplementation(() => ({
|
|
mutateAsync: mockMutateAsync,
|
|
isIdle: !isPendingState,
|
|
isPending: isPendingState,
|
|
}))
|
|
|
|
// A promise that never resolves to keep the pending state
|
|
const pendingPromise = new Promise<void>(() => undefined)
|
|
// When mutateAsync is called, set isPending to true and trigger rerender
|
|
mockMutateAsync.mockImplementation(() => {
|
|
isPendingState = true
|
|
return pendingPromise
|
|
})
|
|
|
|
const props = createDefaultProps()
|
|
const { rerender } = renderWithProviders(<PipelineSettings {...props} />)
|
|
|
|
// Act - Click preview button (sets isPreview.current = true and calls mutateAsync)
|
|
fireEvent.click(screen.getByTestId('preview-btn'))
|
|
|
|
// Update mock and rerender to reflect isPending=true state
|
|
mockUseRunPublishedPipeline.mockReturnValue({
|
|
mutateAsync: mockMutateAsync,
|
|
isIdle: false,
|
|
isPending: true,
|
|
})
|
|
rerender(
|
|
<QueryClientProvider client={createQueryClient()}>
|
|
<PipelineSettings {...props} />
|
|
</QueryClientProvider>,
|
|
)
|
|
|
|
// Assert - isPending && isPreview.current should both be true now
|
|
expect(screen.getByTestId('is-pending')).toHaveTextContent('true')
|
|
})
|
|
|
|
it('should pass isPending=false to ChunkPreview when process is pending (not preview)', async () => {
|
|
// Arrange - isPending is true but isPreview.current is false
|
|
mockUseRunPublishedPipeline.mockReturnValue({
|
|
mutateAsync: mockMutateAsync,
|
|
isIdle: false,
|
|
isPending: true,
|
|
})
|
|
mockMutateAsync.mockReturnValue(new Promise<void>(() => undefined))
|
|
const props = createDefaultProps()
|
|
|
|
// Act
|
|
renderWithProviders(<PipelineSettings {...props} />)
|
|
// Click process (not preview) to set isPreview.current = false
|
|
fireEvent.click(screen.getByRole('button', { name: 'datasetPipeline.operations.saveAndProcess' }))
|
|
|
|
// Assert - isPending && isPreview.current should be false (true && false = false)
|
|
await waitFor(() => {
|
|
expect(screen.getByTestId('is-pending')).toHaveTextContent('false')
|
|
})
|
|
})
|
|
})
|
|
})
|