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 => ({ 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() // 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() expect(screen.getByText('database')).toBeInTheDocument() }) it('should render close button', () => { render() 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( , ) 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( , ) await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledTimes(1) }) rerender() await waitFor(() => { expect(mockMutateAsync).toHaveBeenCalledTimes(2) }) }) it('should handle empty pipelineId', async () => { mockPipelineId.mockReturnValue(undefined) render() 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() // 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() // 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() 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() 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() 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() 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() 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() 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() // 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() // 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() expect(screen.getByText('database')).toBeInTheDocument() }) it('should use credentialId from store', async () => { mockGetState.mockReturnValue({ currentCredentialId: 'custom-credential', }) render() 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() // Content is empty, markdown area should still render but be empty await waitFor(() => { expect(screen.queryByText('Test content')).not.toBeInTheDocument() }) }) }) })