mirror of
https://github.com/langgenius/dify.git
synced 2025-12-19 17:27:16 -05:00
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
360 lines
11 KiB
TypeScript
360 lines
11 KiB
TypeScript
import { render, screen, waitFor } from '@testing-library/react'
|
|
import { fireEvent } from '@testing-library/react'
|
|
import React from 'react'
|
|
import OnlineDocumentPreview from './online-document-preview'
|
|
import type { NotionPage } from '@/models/common'
|
|
import Toast from '@/app/components/base/toast'
|
|
|
|
// Uses __mocks__/react-i18next.ts automatically
|
|
|
|
// Spy on Toast.notify
|
|
const toastNotifySpy = jest.spyOn(Toast, 'notify')
|
|
|
|
// Mock dataset-detail context - needs mock to control return values
|
|
const mockPipelineId = jest.fn()
|
|
jest.mock('@/context/dataset-detail', () => ({
|
|
useDatasetDetailContextWithSelector: (_selector: (s: { dataset: { pipeline_id: string } }) => string) => {
|
|
return mockPipelineId()
|
|
},
|
|
}))
|
|
|
|
// Mock usePreviewOnlineDocument hook - needs mock to control mutation behavior
|
|
const mockMutateAsync = jest.fn()
|
|
const mockUsePreviewOnlineDocument = jest.fn()
|
|
jest.mock('@/service/use-pipeline', () => ({
|
|
usePreviewOnlineDocument: () => mockUsePreviewOnlineDocument(),
|
|
}))
|
|
|
|
// Mock data source store - needs mock to control store state
|
|
const mockCurrentCredentialId = 'credential-123'
|
|
const mockGetState = jest.fn(() => ({
|
|
currentCredentialId: mockCurrentCredentialId,
|
|
}))
|
|
jest.mock('../data-source/store', () => ({
|
|
useDataSourceStore: () => ({
|
|
getState: mockGetState,
|
|
}),
|
|
}))
|
|
|
|
// Test data factory
|
|
const createMockNotionPage = (overrides?: Partial<NotionPage>): NotionPage => ({
|
|
page_id: 'page-123',
|
|
page_name: 'Test Notion Page',
|
|
workspace_id: 'workspace-456',
|
|
type: 'page',
|
|
page_icon: null,
|
|
parent_id: 'parent-789',
|
|
is_bound: true,
|
|
...overrides,
|
|
})
|
|
|
|
const defaultProps = {
|
|
currentPage: createMockNotionPage(),
|
|
datasourceNodeId: 'datasource-node-123',
|
|
hidePreview: jest.fn(),
|
|
}
|
|
|
|
describe('OnlineDocumentPreview', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
mockPipelineId.mockReturnValue('pipeline-123')
|
|
mockUsePreviewOnlineDocument.mockReturnValue({
|
|
mutateAsync: mockMutateAsync,
|
|
isPending: false,
|
|
})
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onSuccess({ content: 'Test content' })
|
|
return Promise.resolve({ content: 'Test content' })
|
|
})
|
|
})
|
|
|
|
describe('Rendering', () => {
|
|
it('should render the component with page information', () => {
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
// i18n mock returns key by default
|
|
expect(screen.getByText('datasetPipeline.addDocuments.stepOne.preview')).toBeInTheDocument()
|
|
expect(screen.getByText('Test Notion Page')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should display page type', () => {
|
|
const currentPage = createMockNotionPage({ type: 'database' })
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
|
|
|
expect(screen.getByText('database')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should render close button', () => {
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
expect(screen.getByRole('button')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Data Fetching', () => {
|
|
it('should call mutateAsync with correct parameters on mount', async () => {
|
|
const currentPage = createMockNotionPage({
|
|
workspace_id: 'ws-123',
|
|
page_id: 'pg-456',
|
|
type: 'page',
|
|
})
|
|
|
|
render(
|
|
<OnlineDocumentPreview
|
|
{...defaultProps}
|
|
currentPage={currentPage}
|
|
datasourceNodeId="node-789"
|
|
/>,
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
{
|
|
workspaceID: 'ws-123',
|
|
pageID: 'pg-456',
|
|
pageType: 'page',
|
|
pipelineId: 'pipeline-123',
|
|
datasourceNodeId: 'node-789',
|
|
credentialId: mockCurrentCredentialId,
|
|
},
|
|
expect.objectContaining({
|
|
onSuccess: expect.any(Function),
|
|
onError: expect.any(Function),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should fetch data again when page_id changes', async () => {
|
|
const currentPage1 = createMockNotionPage({ page_id: 'page-1' })
|
|
const currentPage2 = createMockNotionPage({ page_id: 'page-2' })
|
|
|
|
const { rerender } = render(
|
|
<OnlineDocumentPreview {...defaultProps} currentPage={currentPage1} />,
|
|
)
|
|
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
rerender(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage2} />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|
|
|
|
it('should handle empty pipelineId', async () => {
|
|
mockPipelineId.mockReturnValue(undefined)
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
pipelineId: '',
|
|
}),
|
|
expect.anything(),
|
|
)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Loading State', () => {
|
|
it('should render loading component when isPending is true', () => {
|
|
mockUsePreviewOnlineDocument.mockReturnValue({
|
|
mutateAsync: mockMutateAsync,
|
|
isPending: true,
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
// Loading component renders skeleton
|
|
expect(document.querySelector('.overflow-hidden')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should not render markdown content when loading', () => {
|
|
mockUsePreviewOnlineDocument.mockReturnValue({
|
|
mutateAsync: mockMutateAsync,
|
|
isPending: true,
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
// Content area should not be present
|
|
expect(screen.queryByText('Test content')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
describe('Content Display', () => {
|
|
it('should render markdown content when loaded', async () => {
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onSuccess({ content: 'Markdown content here' })
|
|
return Promise.resolve({ content: 'Markdown content here' })
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
// Markdown component renders the content
|
|
const contentArea = document.querySelector('.overflow-hidden.px-6.py-5')
|
|
expect(contentArea).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should display character count', async () => {
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onSuccess({ content: 'Hello' }) // 5 characters
|
|
return Promise.resolve({ content: 'Hello' })
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
// Real formatNumberAbbreviated returns "5" for numbers < 1000
|
|
expect(screen.getByText(/5/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should format large character counts', async () => {
|
|
const longContent = 'a'.repeat(2500)
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onSuccess({ content: longContent })
|
|
return Promise.resolve({ content: longContent })
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
// Real formatNumberAbbreviated uses lowercase 'k': "2.5k"
|
|
expect(screen.getByText(/2\.5k/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('should show character count based on fetched content', async () => {
|
|
// When content is set via onSuccess, character count is displayed
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onSuccess({ content: 'Test content' }) // 12 characters
|
|
return Promise.resolve({ content: 'Test content' })
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/12/)).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Error Handling', () => {
|
|
it('should show toast notification on error', async () => {
|
|
const errorMessage = 'Failed to fetch document'
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onError(new Error(errorMessage))
|
|
// Return a resolved promise to avoid unhandled rejection
|
|
return Promise.resolve()
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
expect(toastNotifySpy).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: errorMessage,
|
|
})
|
|
})
|
|
})
|
|
|
|
it('should handle network errors', async () => {
|
|
const networkError = new Error('Network Error')
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onError(networkError)
|
|
// Return a resolved promise to avoid unhandled rejection
|
|
return Promise.resolve()
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
expect(toastNotifySpy).toHaveBeenCalledWith({
|
|
type: 'error',
|
|
message: 'Network Error',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call hidePreview when close button is clicked', () => {
|
|
const hidePreview = jest.fn()
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} hidePreview={hidePreview} />)
|
|
|
|
// Find the close button in the header area (not toast buttons)
|
|
const headerArea = document.querySelector('.flex.gap-x-2.border-b')
|
|
const closeButton = headerArea?.querySelector('button')
|
|
expect(closeButton).toBeInTheDocument()
|
|
fireEvent.click(closeButton!)
|
|
|
|
expect(hidePreview).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle undefined page_name', () => {
|
|
const currentPage = createMockNotionPage({ page_name: '' })
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
|
|
|
// Find the close button in the header area
|
|
const headerArea = document.querySelector('.flex.gap-x-2.border-b')
|
|
const closeButton = headerArea?.querySelector('button')
|
|
expect(closeButton).toBeInTheDocument()
|
|
})
|
|
|
|
it('should handle different page types', () => {
|
|
const currentPage = createMockNotionPage({ type: 'database' })
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} currentPage={currentPage} />)
|
|
|
|
expect(screen.getByText('database')).toBeInTheDocument()
|
|
})
|
|
|
|
it('should use credentialId from store', async () => {
|
|
mockGetState.mockReturnValue({
|
|
currentCredentialId: 'custom-credential',
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
await waitFor(() => {
|
|
expect(mockMutateAsync).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
credentialId: 'custom-credential',
|
|
}),
|
|
expect.anything(),
|
|
)
|
|
})
|
|
})
|
|
|
|
it('should not render markdown content when content is empty and not pending', async () => {
|
|
mockMutateAsync.mockImplementation((params, callbacks) => {
|
|
callbacks.onSuccess({ content: '' })
|
|
return Promise.resolve({ content: '' })
|
|
})
|
|
mockUsePreviewOnlineDocument.mockReturnValue({
|
|
mutateAsync: mockMutateAsync,
|
|
isPending: false,
|
|
})
|
|
|
|
render(<OnlineDocumentPreview {...defaultProps} />)
|
|
|
|
// Content is empty, markdown area should still render but be empty
|
|
await waitFor(() => {
|
|
expect(screen.queryByText('Test content')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
})
|