From 7368797a119b224a920ec1d5b58c970c393c8ff5 Mon Sep 17 00:00:00 2001 From: LordMathis Date: Sun, 6 Jul 2025 00:08:42 +0200 Subject: [PATCH] Add tests for ContentView and MarkdownPreview components --- .../components/editor/ContentView.test.tsx | 225 +++++++++++++ .../editor/MarkdownPreview.test.tsx | 318 ++++++++++++++++++ app/src/components/editor/MarkdownPreview.tsx | 6 +- 3 files changed, 548 insertions(+), 1 deletion(-) create mode 100644 app/src/components/editor/ContentView.test.tsx create mode 100644 app/src/components/editor/MarkdownPreview.test.tsx diff --git a/app/src/components/editor/ContentView.test.tsx b/app/src/components/editor/ContentView.test.tsx new file mode 100644 index 0000000..cb41b81 --- /dev/null +++ b/app/src/components/editor/ContentView.test.tsx @@ -0,0 +1,225 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render } from '../../test/utils'; +import ContentView from './ContentView'; +import { Theme } from '@/types/models'; + +// Mock child components +vi.mock('./Editor', () => ({ + default: ({ + content, + selectedFile, + }: { + content: string; + selectedFile: string; + }) => ( +
+ Editor - {selectedFile} - {content} +
+ ), +})); + +vi.mock('./MarkdownPreview', () => ({ + default: ({ content }: { content: string }) => ( +
Preview - {content}
+ ), +})); + +// Mock contexts +vi.mock('../../contexts/WorkspaceContext', () => ({ + useWorkspace: vi.fn(), +})); + +// Mock utils +vi.mock('../../utils/fileHelpers', () => ({ + getFileUrl: vi.fn( + (workspace: string, file: string) => `http://test.com/${workspace}/${file}` + ), + isImageFile: vi.fn(), +})); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( +
{children}
+); + +describe('ContentView', () => { + const mockHandleContentChange = vi.fn(); + const mockHandleSave = vi.fn(); + const mockHandleFileSelect = vi.fn(); + + const mockCurrentWorkspace = { + id: 1, + name: 'test-workspace', + createdAt: '2024-01-01T00:00:00Z', + theme: Theme.Light, + autoSave: false, + showHiddenFiles: false, + gitEnabled: false, + gitUrl: '', + gitUser: '', + gitToken: '', + gitAutoCommit: false, + gitCommitMsgTemplate: '${action} ${filename}', + gitCommitName: '', + gitCommitEmail: '', + }; + + beforeEach(async () => { + vi.clearAllMocks(); + + const { useWorkspace } = await import('../../contexts/WorkspaceContext'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: mockCurrentWorkspace, + workspaces: [], + settings: mockCurrentWorkspace, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { isImageFile } = await import('../../utils/fileHelpers'); + vi.mocked(isImageFile).mockReturnValue(false); + }); + + it('shows no workspace message when no workspace selected', async () => { + const { useWorkspace } = await import('../../contexts/WorkspaceContext'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: null, + workspaces: [], + settings: mockCurrentWorkspace, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const { getByText } = render( + + + + ); + + expect(getByText('No workspace selected.')).toBeInTheDocument(); + }); + + it('shows no file message when no file selected', () => { + const { getByText } = render( + + + + ); + + expect(getByText('No file selected.')).toBeInTheDocument(); + }); + + it('renders editor when activeTab is source', () => { + const { getByTestId } = render( + + + + ); + + const editor = getByTestId('editor'); + expect(editor).toBeInTheDocument(); + expect(editor).toHaveTextContent('Editor - test.md - Test content'); + }); + + it('renders markdown preview when activeTab is preview', () => { + const { getByTestId } = render( + + + + ); + + const preview = getByTestId('markdown-preview'); + expect(preview).toBeInTheDocument(); + expect(preview).toHaveTextContent('Preview - # Test content'); + }); + + it('renders image preview for image files', async () => { + const { isImageFile } = await import('../../utils/fileHelpers'); + vi.mocked(isImageFile).mockReturnValue(true); + + const { container } = render( + + + + ); + + const imagePreview = container.querySelector('.image-preview'); + expect(imagePreview).toBeInTheDocument(); + + const img = container.querySelector('img'); + expect(img).toBeInTheDocument(); + expect(img).toHaveAttribute( + 'src', + 'http://test.com/test-workspace/image.png' + ); + expect(img).toHaveAttribute('alt', 'image.png'); + }); + + it('ignores activeTab for image files', async () => { + const { isImageFile } = await import('../../utils/fileHelpers'); + vi.mocked(isImageFile).mockReturnValue(true); + + const { container, queryByTestId } = render( + + + + ); + + // Should show image preview regardless of activeTab + const imagePreview = container.querySelector('.image-preview'); + expect(imagePreview).toBeInTheDocument(); + + // Should not render editor or markdown preview + expect(queryByTestId('editor')).not.toBeInTheDocument(); + expect(queryByTestId('markdown-preview')).not.toBeInTheDocument(); + }); +}); diff --git a/app/src/components/editor/MarkdownPreview.test.tsx b/app/src/components/editor/MarkdownPreview.test.tsx new file mode 100644 index 0000000..29d131c --- /dev/null +++ b/app/src/components/editor/MarkdownPreview.test.tsx @@ -0,0 +1,318 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { + render as rtlRender, + screen, + fireEvent, + waitFor, +} from '@testing-library/react'; +import React from 'react'; +import { MantineProvider } from '@mantine/core'; +import MarkdownPreview from './MarkdownPreview'; +import { notifications } from '@mantine/notifications'; +import { Theme, DEFAULT_WORKSPACE_SETTINGS } from '../../types/models'; + +// Mock notifications +vi.mock('@mantine/notifications', () => ({ + notifications: { + show: vi.fn(), + }, +})); + +// Mock useWorkspace hook +vi.mock('../../hooks/useWorkspace', () => ({ + useWorkspace: vi.fn(), +})); + +// Mock the remarkWikiLinks utility +vi.mock('../../utils/remarkWikiLinks', () => ({ + remarkWikiLinks: vi.fn(() => () => {}), +})); + +// Mock window.API_BASE_URL +Object.defineProperty(window, 'API_BASE_URL', { + value: 'http://localhost:3000', + writable: true, +}); + +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +const render = (ui: React.ReactElement) => { + return rtlRender(ui, { wrapper: TestWrapper }); +}; + +describe('MarkdownPreview', () => { + const mockHandleFileSelect = vi.fn(); + const mockNotificationsShow = vi.mocked(notifications.show); + + beforeEach(async () => { + vi.clearAllMocks(); + + // Setup useWorkspace mock + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: { + id: 1, + name: 'test-workspace', + theme: Theme.Light, + autoSave: false, + showHiddenFiles: false, + gitEnabled: false, + gitUrl: '', + gitUser: '', + gitToken: '', + gitAutoCommit: false, + gitCommitMsgTemplate: '', + gitCommitName: '', + gitCommitEmail: '', + createdAt: '2023-01-01T00:00:00Z', + lastOpenedFilePath: '', + }, + workspaces: [], + settings: DEFAULT_WORKSPACE_SETTINGS, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + }); + + it('renders basic markdown content', async () => { + const content = '# Hello World\n\nThis is a test.'; + + render( + + ); + + await waitFor(() => { + expect(screen.getByText('Hello World')).toBeInTheDocument(); + expect(screen.getByText('This is a test.')).toBeInTheDocument(); + }); + }); + + it('renders code blocks with syntax highlighting', async () => { + const content = '```javascript\nconst hello = "world";\n```'; + + render( + + ); + + await waitFor(() => { + // Check for the code element containing the text pieces + const codeElement = screen.getByText((_, element) => { + return !!( + element?.tagName.toLowerCase() === 'code' && + element?.textContent?.includes('const') && + element?.textContent?.includes('hello') && + element?.textContent?.includes('world') + ); + }); + expect(codeElement).toBeInTheDocument(); + expect(codeElement.closest('pre')).toBeInTheDocument(); + }); + }); + + it('handles image loading errors gracefully', async () => { + const content = '![Test Image](invalid-image.jpg)'; + + render( + + ); + + await waitFor(() => { + const img = screen.getByRole('img'); + expect(img).toBeInTheDocument(); + + // Simulate image load error + fireEvent.error(img); + + expect(img).toHaveAttribute('alt', 'Failed to load image'); + }); + }); + + it('handles internal link clicks and calls handleFileSelect', async () => { + const content = '[Test Link](http://localhost:3000/internal/test-file.md)'; + + render( + + ); + + await waitFor(() => { + const link = screen.getByText('Test Link'); + expect(link).toBeInTheDocument(); + + fireEvent.click(link); + + expect(mockHandleFileSelect).toHaveBeenCalledWith('test-file.md'); + }); + }); + + it('shows notification for non-existent file links', async () => { + const content = + '[Missing File](http://localhost:3000/notfound/missing-file.md)'; + + render( + + ); + + await waitFor(() => { + const link = screen.getByText('Missing File'); + fireEvent.click(link); + + expect(mockNotificationsShow).toHaveBeenCalledWith({ + title: 'File Not Found', + message: 'The file "missing-file.md" does not exist.', + color: 'red', + }); + expect(mockHandleFileSelect).not.toHaveBeenCalled(); + }); + }); + + it('handles external links normally without interference', async () => { + const content = '[External Link](https://example.com)'; + + render( + + ); + + await waitFor(() => { + const link = screen.getByText('External Link'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + + // Click should be prevented but no file selection should occur + fireEvent.click(link); + expect(mockHandleFileSelect).not.toHaveBeenCalled(); + expect(mockNotificationsShow).not.toHaveBeenCalled(); + }); + }); + + it('does not process content when no workspace is available', async () => { + const { useWorkspace } = await import('../../hooks/useWorkspace'); + vi.mocked(useWorkspace).mockReturnValue({ + currentWorkspace: null, + workspaces: [], + settings: DEFAULT_WORKSPACE_SETTINGS, + updateSettings: vi.fn(), + loading: false, + colorScheme: 'light', + updateColorScheme: vi.fn(), + switchWorkspace: vi.fn(), + deleteCurrentWorkspace: vi.fn(), + }); + + const content = '# Test Content'; + + render( + + ); + + // Should render empty content when no workspace + const markdownPreview = screen.getByTestId('markdown-preview'); + expect(markdownPreview).toBeEmptyDOMElement(); + }); + + it('handles empty content gracefully', async () => { + render( + + ); + + await waitFor(() => { + const markdownPreview = screen.getByTestId('markdown-preview'); + expect(markdownPreview).toBeInTheDocument(); + }); + }); + + it('updates content when markdown changes', async () => { + const { rerender } = render( + + ); + + await waitFor(() => { + expect(screen.getByText('First Content')).toBeInTheDocument(); + }); + + rerender( + + + + ); + + await waitFor(() => { + expect(screen.getByText('Updated Content')).toBeInTheDocument(); + expect(screen.queryByText('First Content')).not.toBeInTheDocument(); + }); + }); + + it('handles markdown processing errors gracefully', () => { + // Mock console.error to avoid noise in test output + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + + // Create content that might cause processing issues + const problematicContent = '# Test\n\n```invalid-syntax\nbroken code\n```'; + + render( + + ); + + // Should still render something even if processing has issues + const markdownPreview = screen.getByTestId('markdown-preview'); + expect(markdownPreview).toBeInTheDocument(); + + consoleSpy.mockRestore(); + }); + + it('handles URL decoding for file paths correctly', async () => { + const encodedContent = + '[Test Link](http://localhost:3000/internal/test%20file%20with%20spaces.md)'; + + render( + + ); + + await waitFor(() => { + const link = screen.getByText('Test Link'); + fireEvent.click(link); + + expect(mockHandleFileSelect).toHaveBeenCalledWith( + 'test file with spaces.md' + ); + }); + }); +}); diff --git a/app/src/components/editor/MarkdownPreview.tsx b/app/src/components/editor/MarkdownPreview.tsx index 83e4c4b..463f4b2 100644 --- a/app/src/components/editor/MarkdownPreview.tsx +++ b/app/src/components/editor/MarkdownPreview.tsx @@ -135,7 +135,11 @@ const MarkdownPreview: React.FC = ({ void processContent(); }, [content, processor, currentWorkspace]); - return
{processedContent}
; + return ( +
+ {processedContent} +
+ ); }; export default MarkdownPreview;