dify/web/app/components/datasets/create/notion-page-preview/index.spec.tsx
Stephen Zhou f2842da397
chore(web): new lint setup (#30020)
Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
2025-12-23 16:58:55 +08:00

1151 lines
37 KiB
TypeScript

import type { MockedFunction } from 'vitest'
import type { NotionPage } from '@/models/common'
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fetchNotionPagePreview } from '@/service/datasets'
import NotionPagePreview from './index'
// Mock the fetchNotionPagePreview service
vi.mock('@/service/datasets', () => ({
fetchNotionPagePreview: vi.fn(),
}))
const mockFetchNotionPagePreview = fetchNotionPagePreview as MockedFunction<typeof fetchNotionPagePreview>
// Factory function to create mock NotionPage objects
const createMockNotionPage = (overrides: Partial<NotionPage> = {}): NotionPage => {
return {
page_id: 'page-123',
page_name: 'Test Page',
page_icon: null,
parent_id: 'parent-123',
type: 'page',
is_bound: false,
workspace_id: 'workspace-123',
...overrides,
}
}
// Factory function to create NotionPage with emoji icon
const createMockNotionPageWithEmojiIcon = (emoji: string, overrides: Partial<NotionPage> = {}): NotionPage => {
return createMockNotionPage({
page_icon: {
type: 'emoji',
url: null,
emoji,
},
...overrides,
})
}
// Factory function to create NotionPage with URL icon
const createMockNotionPageWithUrlIcon = (url: string, overrides: Partial<NotionPage> = {}): NotionPage => {
return createMockNotionPage({
page_icon: {
type: 'url',
url,
emoji: null,
},
...overrides,
})
}
// Helper to render NotionPagePreview with default props and wait for async updates
const renderNotionPagePreview = async (
props: Partial<{
currentPage?: NotionPage
notionCredentialId: string
hidePreview: () => void
}> = {},
waitForContent = true,
) => {
const defaultProps = {
currentPage: createMockNotionPage(),
notionCredentialId: 'credential-123',
hidePreview: vi.fn(),
...props,
}
const result = render(<NotionPagePreview {...defaultProps} />)
// Wait for async state updates to complete if needed
if (waitForContent && defaultProps.currentPage) {
await waitFor(() => {
// Wait for loading to finish
expect(result.container.querySelector('.spin-animation')).not.toBeInTheDocument()
})
}
return {
...result,
props: defaultProps,
}
}
// Helper to find the loading spinner element
const findLoadingSpinner = (container: HTMLElement) => {
return container.querySelector('.spin-animation')
}
// ============================================================================
// NotionPagePreview Component Tests
// ============================================================================
// Note: Branch coverage is ~88% because line 29 (`if (!currentPage) return`)
// is defensive code that cannot be reached - getPreviewContent is only called
// from useEffect when currentPage is truthy.
// ============================================================================
describe('NotionPagePreview', () => {
beforeEach(() => {
vi.clearAllMocks()
// Default successful API response
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Preview content here' })
})
afterEach(async () => {
// Wait for any pending state updates to complete
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0))
})
})
// --------------------------------------------------------------------------
// Rendering Tests - Verify component renders properly
// --------------------------------------------------------------------------
describe('Rendering', () => {
it('should render without crashing', async () => {
// Arrange & Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
it('should render page preview header', async () => {
// Arrange & Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
it('should render close button with XMarkIcon', async () => {
// Arrange & Act
const { container } = await renderNotionPagePreview()
// Assert
const closeButton = container.querySelector('.cursor-pointer')
expect(closeButton).toBeInTheDocument()
const xMarkIcon = closeButton?.querySelector('svg')
expect(xMarkIcon).toBeInTheDocument()
})
it('should render page name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: 'My Notion Page' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('My Notion Page')).toBeInTheDocument()
})
it('should apply correct CSS classes to container', async () => {
// Arrange & Act
const { container } = await renderNotionPagePreview()
// Assert
const wrapper = container.firstChild as HTMLElement
expect(wrapper).toHaveClass('h-full')
})
it('should render NotionIcon component', async () => {
// Arrange
const page = createMockNotionPage()
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - NotionIcon should be rendered (either as img or div or svg)
const iconContainer = container.querySelector('.mr-1.shrink-0')
expect(iconContainer).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// NotionIcon Rendering Tests
// --------------------------------------------------------------------------
describe('NotionIcon Rendering', () => {
it('should render default icon when page_icon is null', async () => {
// Arrange
const page = createMockNotionPage({ page_icon: null })
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should render RiFileTextLine icon (svg)
const svgIcon = container.querySelector('svg')
expect(svgIcon).toBeInTheDocument()
})
it('should render emoji icon when page_icon has emoji type', async () => {
// Arrange
const page = createMockNotionPageWithEmojiIcon('📝')
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('📝')).toBeInTheDocument()
})
it('should render image icon when page_icon has url type', async () => {
// Arrange
const page = createMockNotionPageWithUrlIcon('https://example.com/icon.png')
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert
const img = container.querySelector('img[alt="page icon"]')
expect(img).toBeInTheDocument()
expect(img).toHaveAttribute('src', 'https://example.com/icon.png')
})
})
// --------------------------------------------------------------------------
// Loading State Tests
// --------------------------------------------------------------------------
describe('Loading State', () => {
it('should show loading indicator initially', async () => {
// Arrange - Delay API response to keep loading state
mockFetchNotionPagePreview.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve({ content: 'test' }), 100)),
)
// Act - Don't wait for content to load
const { container } = await renderNotionPagePreview({}, false)
// Assert - Loading should be visible initially
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).toBeInTheDocument()
})
it('should hide loading indicator after content loads', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Loaded content' })
// Act
const { container } = await renderNotionPagePreview()
// Assert
expect(screen.getByText('Loaded content')).toBeInTheDocument()
// Loading should be gone
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).not.toBeInTheDocument()
})
it('should show loading when currentPage changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1', page_name: 'Page 1' })
const page2 = createMockNotionPage({ page_id: 'page-2', page_name: 'Page 2' })
let resolveFirst: (value: { content: string }) => void
let resolveSecond: (value: { content: string }) => void
mockFetchNotionPagePreview
.mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve }))
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
// Act - Initial render
const { rerender, container } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
// First page loading - spinner should be visible
expect(findLoadingSpinner(container)).toBeInTheDocument()
// Resolve first page
await act(async () => {
resolveFirst({ content: 'Content 1' })
})
await waitFor(() => {
expect(screen.getByText('Content 1')).toBeInTheDocument()
})
// Rerender with new page
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
// Should show loading again
await waitFor(() => {
expect(findLoadingSpinner(container)).toBeInTheDocument()
})
// Resolve second page
await act(async () => {
resolveSecond({ content: 'Content 2' })
})
await waitFor(() => {
expect(screen.getByText('Content 2')).toBeInTheDocument()
})
})
})
// --------------------------------------------------------------------------
// API Call Tests
// --------------------------------------------------------------------------
describe('API Calls', () => {
it('should call fetchNotionPagePreview with correct parameters', async () => {
// Arrange
const page = createMockNotionPage({
page_id: 'test-page-id',
type: 'database',
})
// Act
await renderNotionPagePreview({
currentPage: page,
notionCredentialId: 'test-credential-id',
})
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith({
pageID: 'test-page-id',
pageType: 'database',
credentialID: 'test-credential-id',
})
})
it('should not call fetchNotionPagePreview when currentPage is undefined', async () => {
// Arrange & Act
await renderNotionPagePreview({ currentPage: undefined }, false)
// Assert
expect(mockFetchNotionPagePreview).not.toHaveBeenCalled()
})
it('should call fetchNotionPagePreview again when currentPage changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith({
pageID: 'page-1',
pageType: 'page',
credentialID: 'cred-123',
})
})
await act(async () => {
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
// Assert
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith({
pageID: 'page-2',
pageType: 'page',
credentialID: 'cred-123',
})
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(2)
})
})
it('should handle API success and display content', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Notion page preview content from API' })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('Notion page preview content from API')).toBeInTheDocument()
})
it('should handle API error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('Network error'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert - Component should not crash
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
// Header should still render
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
it('should handle empty content response', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: '' })
// Act
const { container } = await renderNotionPagePreview()
// Assert - Should still render without loading
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).not.toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// User Interactions Tests
// --------------------------------------------------------------------------
describe('User Interactions', () => {
it('should call hidePreview when close button is clicked', async () => {
// Arrange
const hidePreview = vi.fn()
const { container } = await renderNotionPagePreview({ hidePreview })
// Act
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
// Assert
expect(hidePreview).toHaveBeenCalledTimes(1)
})
it('should handle multiple clicks on close button', async () => {
// Arrange
const hidePreview = vi.fn()
const { container } = await renderNotionPagePreview({ hidePreview })
// Act
const closeButton = container.querySelector('.cursor-pointer') as HTMLElement
fireEvent.click(closeButton)
fireEvent.click(closeButton)
fireEvent.click(closeButton)
// Assert
expect(hidePreview).toHaveBeenCalledTimes(3)
})
})
// --------------------------------------------------------------------------
// State Management Tests
// --------------------------------------------------------------------------
describe('State Management', () => {
it('should initialize with loading state true', async () => {
// Arrange - Keep loading indefinitely (never resolves)
mockFetchNotionPagePreview.mockImplementation(() => new Promise(() => { /* intentionally empty */ }))
// Act - Don't wait for content
const { container } = await renderNotionPagePreview({}, false)
// Assert
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).toBeInTheDocument()
})
it('should update previewContent state after successful fetch', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'New preview content' })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('New preview content')).toBeInTheDocument()
})
it('should reset loading to true when currentPage changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
mockFetchNotionPagePreview
.mockResolvedValueOnce({ content: 'Content 1' })
.mockImplementationOnce(() => new Promise(() => { /* never resolves */ }))
// Act
const { rerender, container } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
await waitFor(() => {
expect(screen.getByText('Content 1')).toBeInTheDocument()
})
// Change page
await act(async () => {
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
// Assert - Loading should be shown again
await waitFor(() => {
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).toBeInTheDocument()
})
})
it('should replace old content with new content when page changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
let resolveSecond: (value: { content: string }) => void
mockFetchNotionPagePreview
.mockResolvedValueOnce({ content: 'Content 1' })
.mockImplementationOnce(() => new Promise((resolve) => { resolveSecond = resolve }))
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
await waitFor(() => {
expect(screen.getByText('Content 1')).toBeInTheDocument()
})
// Change page
await act(async () => {
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
// Resolve second fetch
await act(async () => {
resolveSecond({ content: 'Content 2' })
})
await waitFor(() => {
expect(screen.getByText('Content 2')).toBeInTheDocument()
expect(screen.queryByText('Content 1')).not.toBeInTheDocument()
})
})
})
// --------------------------------------------------------------------------
// Props Testing
// --------------------------------------------------------------------------
describe('Props', () => {
describe('currentPage prop', () => {
it('should render correctly with currentPage prop', async () => {
// Arrange
const page = createMockNotionPage({ page_name: 'My Test Page' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('My Test Page')).toBeInTheDocument()
})
it('should render correctly without currentPage prop (undefined)', async () => {
// Arrange & Act
await renderNotionPagePreview({ currentPage: undefined }, false)
// Assert - Header should still render
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
it('should handle page with empty name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: '' })
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should not crash
expect(container.firstChild).toBeInTheDocument()
})
it('should handle page with very long name', async () => {
// Arrange
const longName = 'a'.repeat(200)
const page = createMockNotionPage({ page_name: longName })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText(longName)).toBeInTheDocument()
})
it('should handle page with special characters in name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: 'Page with <special> & "chars"' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('Page with <special> & "chars"')).toBeInTheDocument()
})
it('should handle page with unicode characters in name', async () => {
// Arrange
const page = createMockNotionPage({ page_name: '中文页面名称 🚀 日本語' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('中文页面名称 🚀 日本語')).toBeInTheDocument()
})
})
describe('notionCredentialId prop', () => {
it('should pass notionCredentialId to API call', async () => {
// Arrange
const page = createMockNotionPage()
// Act
await renderNotionPagePreview({
currentPage: page,
notionCredentialId: 'my-credential-id',
})
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ credentialID: 'my-credential-id' }),
)
})
})
describe('hidePreview prop', () => {
it('should accept hidePreview callback', async () => {
// Arrange
const hidePreview = vi.fn()
// Act
await renderNotionPagePreview({ hidePreview })
// Assert - No errors thrown
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
})
})
// --------------------------------------------------------------------------
// Edge Cases Tests
// --------------------------------------------------------------------------
describe('Edge Cases', () => {
it('should handle page with undefined page_id', async () => {
// Arrange
const page = createMockNotionPage({ page_id: undefined as unknown as string })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert - API should still be called (with undefined pageID)
expect(mockFetchNotionPagePreview).toHaveBeenCalled()
})
it('should handle page with empty string page_id', async () => {
// Arrange
const page = createMockNotionPage({ page_id: '' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageID: '' }),
)
})
it('should handle very long preview content', async () => {
// Arrange
const longContent = 'x'.repeat(10000)
mockFetchNotionPagePreview.mockResolvedValue({ content: longContent })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText(longContent)).toBeInTheDocument()
})
it('should handle preview content with special characters safely', async () => {
// Arrange
const specialContent = '<script>alert("xss")</script>\n\t& < > "'
mockFetchNotionPagePreview.mockResolvedValue({ content: specialContent })
// Act
const { container } = await renderNotionPagePreview()
// Assert - Should render as text, not execute scripts
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
expect(contentDiv?.textContent).toContain('alert')
})
it('should handle preview content with unicode', async () => {
// Arrange
const unicodeContent = '中文内容 🚀 émojis & spëcîal çhàrs'
mockFetchNotionPagePreview.mockResolvedValue({ content: unicodeContent })
// Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText(unicodeContent)).toBeInTheDocument()
})
it('should handle preview content with newlines', async () => {
// Arrange
const multilineContent = 'Line 1\nLine 2\nLine 3'
mockFetchNotionPagePreview.mockResolvedValue({ content: multilineContent })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
expect(contentDiv?.textContent).toContain('Line 1')
expect(contentDiv?.textContent).toContain('Line 2')
expect(contentDiv?.textContent).toContain('Line 3')
})
it('should handle null content from API', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: null as unknown as string })
// Act
const { container } = await renderNotionPagePreview()
// Assert - Should not crash
expect(container.firstChild).toBeInTheDocument()
})
it('should handle different page types', async () => {
// Arrange
const databasePage = createMockNotionPage({ type: 'database' })
// Act
await renderNotionPagePreview({ currentPage: databasePage })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'database' }),
)
})
})
// --------------------------------------------------------------------------
// Side Effects and Cleanup Tests
// --------------------------------------------------------------------------
describe('Side Effects and Cleanup', () => {
it('should trigger effect when currentPage prop changes', async () => {
// Arrange
const page1 = createMockNotionPage({ page_id: 'page-1' })
const page2 = createMockNotionPage({ page_id: 'page-2' })
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page1} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(1)
})
await act(async () => {
rerender(<NotionPagePreview currentPage={page2} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
// Assert
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(2)
})
})
it('should not trigger effect when hidePreview changes', async () => {
// Arrange
const page = createMockNotionPage()
const hidePreview1 = vi.fn()
const hidePreview2 = vi.fn()
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page} notionCredentialId="cred-123" hidePreview={hidePreview1} />,
)
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(1)
})
await act(async () => {
rerender(<NotionPagePreview currentPage={page} notionCredentialId="cred-123" hidePreview={hidePreview2} />)
})
// Assert - Should not call API again (currentPage didn't change by reference)
// Note: Since currentPage is the same object, effect should not re-run
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(1)
})
it('should not trigger effect when notionCredentialId changes', async () => {
// Arrange
const page = createMockNotionPage()
// Act
const { rerender } = render(
<NotionPagePreview currentPage={page} notionCredentialId="cred-1" hidePreview={vi.fn()} />,
)
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(1)
})
await act(async () => {
rerender(<NotionPagePreview currentPage={page} notionCredentialId="cred-2" hidePreview={vi.fn()} />)
})
// Assert - Should not call API again (only currentPage is in dependency array)
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(1)
})
it('should handle rapid page changes', async () => {
// Arrange
const pages = Array.from({ length: 5 }, (_, i) =>
createMockNotionPage({ page_id: `page-${i}` }))
// Act
const { rerender } = render(
<NotionPagePreview currentPage={pages[0]} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
// Rapidly change pages
for (let i = 1; i < pages.length; i++) {
await act(async () => {
rerender(<NotionPagePreview currentPage={pages[i]} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
}
// Assert - Should have called API for each page
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(5)
})
})
it('should handle unmount during loading', async () => {
// Arrange
mockFetchNotionPagePreview.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve({ content: 'delayed' }), 1000)),
)
// Act - Don't wait for content
const { unmount } = await renderNotionPagePreview({}, false)
// Unmount before API resolves
unmount()
// Assert - No errors should be thrown
expect(true).toBe(true)
})
it('should handle page changing from defined to undefined', async () => {
// Arrange
const page = createMockNotionPage()
// Act
const { rerender, container } = render(
<NotionPagePreview currentPage={page} notionCredentialId="cred-123" hidePreview={vi.fn()} />,
)
await waitFor(() => {
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(1)
})
await act(async () => {
rerender(<NotionPagePreview currentPage={undefined} notionCredentialId="cred-123" hidePreview={vi.fn()} />)
})
// Assert - Should not crash, API should not be called again
expect(container.firstChild).toBeInTheDocument()
expect(mockFetchNotionPagePreview).toHaveBeenCalledTimes(1)
})
})
// --------------------------------------------------------------------------
// Accessibility Tests
// --------------------------------------------------------------------------
describe('Accessibility', () => {
it('should have clickable close button with visual indicator', async () => {
// Arrange & Act
const { container } = await renderNotionPagePreview()
// Assert
const closeButton = container.querySelector('.cursor-pointer')
expect(closeButton).toBeInTheDocument()
expect(closeButton).toHaveClass('cursor-pointer')
})
it('should have proper heading structure', async () => {
// Arrange & Act
await renderNotionPagePreview()
// Assert
expect(screen.getByText('datasetCreation.stepOne.pagePreview')).toBeInTheDocument()
})
})
// --------------------------------------------------------------------------
// Error Handling Tests
// --------------------------------------------------------------------------
describe('Error Handling', () => {
it('should not crash on API network error', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('Network Error'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert - Component should still render
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should not crash on API timeout', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('Timeout'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should not crash on malformed API response', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({} as { content: string })
// Act
const { container } = await renderNotionPagePreview()
// Assert
expect(container.firstChild).toBeInTheDocument()
})
it('should handle 404 error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('404 Not Found'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should handle 500 error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('500 Internal Server Error'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
it('should handle authorization error gracefully', async () => {
// Arrange
mockFetchNotionPagePreview.mockRejectedValue(new Error('401 Unauthorized'))
// Act
const { container } = await renderNotionPagePreview({}, false)
// Assert
await waitFor(() => {
expect(container.firstChild).toBeInTheDocument()
})
})
})
// --------------------------------------------------------------------------
// Page Type Variations Tests
// --------------------------------------------------------------------------
describe('Page Type Variations', () => {
it('should handle page type', async () => {
// Arrange
const page = createMockNotionPage({ type: 'page' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'page' }),
)
})
it('should handle database type', async () => {
// Arrange
const page = createMockNotionPage({ type: 'database' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'database' }),
)
})
it('should handle unknown type', async () => {
// Arrange
const page = createMockNotionPage({ type: 'unknown_type' })
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(mockFetchNotionPagePreview).toHaveBeenCalledWith(
expect.objectContaining({ pageType: 'unknown_type' }),
)
})
})
// --------------------------------------------------------------------------
// Icon Type Variations Tests
// --------------------------------------------------------------------------
describe('Icon Type Variations', () => {
it('should handle page with null icon', async () => {
// Arrange
const page = createMockNotionPage({ page_icon: null })
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should render default icon
const svgIcon = container.querySelector('svg')
expect(svgIcon).toBeInTheDocument()
})
it('should handle page with emoji icon object', async () => {
// Arrange
const page = createMockNotionPageWithEmojiIcon('📄')
// Act
await renderNotionPagePreview({ currentPage: page })
// Assert
expect(screen.getByText('📄')).toBeInTheDocument()
})
it('should handle page with url icon object', async () => {
// Arrange
const page = createMockNotionPageWithUrlIcon('https://example.com/custom-icon.png')
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert
const img = container.querySelector('img[alt="page icon"]')
expect(img).toBeInTheDocument()
expect(img).toHaveAttribute('src', 'https://example.com/custom-icon.png')
})
it('should handle page with icon object having null values', async () => {
// Arrange
const page = createMockNotionPage({
page_icon: {
type: null,
url: null,
emoji: null,
},
})
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Should render, likely with default/fallback
expect(container.firstChild).toBeInTheDocument()
})
it('should handle page with icon object having empty url', async () => {
// Arrange
// Suppress console.error for this test as we're intentionally testing empty src edge case
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(vi.fn())
const page = createMockNotionPage({
page_icon: {
type: 'url',
url: '',
emoji: null,
},
})
// Act
const { container } = await renderNotionPagePreview({ currentPage: page })
// Assert - Component should not crash, may render img or fallback
expect(container.firstChild).toBeInTheDocument()
// NotionIcon renders img when type is 'url'
const img = container.querySelector('img[alt="page icon"]')
if (img)
expect(img).toBeInTheDocument()
// Restore console.error
consoleErrorSpy.mockRestore()
})
})
// --------------------------------------------------------------------------
// Content Display Tests
// --------------------------------------------------------------------------
describe('Content Display', () => {
it('should display content in fileContent div with correct class', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: 'Test content' })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
expect(contentDiv).toHaveTextContent('Test content')
})
it('should preserve whitespace in content', async () => {
// Arrange
const contentWithWhitespace = ' indented content\n more indent'
mockFetchNotionPagePreview.mockResolvedValue({ content: contentWithWhitespace })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
// The CSS class has white-space: pre-line
expect(contentDiv?.textContent).toContain('indented content')
})
it('should display empty string content without loading', async () => {
// Arrange
mockFetchNotionPagePreview.mockResolvedValue({ content: '' })
// Act
const { container } = await renderNotionPagePreview()
// Assert
const loadingElement = findLoadingSpinner(container)
expect(loadingElement).not.toBeInTheDocument()
const contentDiv = container.querySelector('[class*="fileContent"]')
expect(contentDiv).toBeInTheDocument()
expect(contentDiv?.textContent).toBe('')
})
})
})