Add tests for ContentView and MarkdownPreview components

This commit is contained in:
2025-07-06 00:08:42 +02:00
parent 2747f51293
commit 7368797a11
3 changed files with 548 additions and 1 deletions

View File

@@ -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;
}) => (
<div data-testid="editor">
Editor - {selectedFile} - {content}
</div>
),
}));
vi.mock('./MarkdownPreview', () => ({
default: ({ content }: { content: string }) => (
<div data-testid="markdown-preview">Preview - {content}</div>
),
}));
// 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 }) => (
<div>{children}</div>
);
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(
<TestWrapper>
<ContentView
activeTab="source"
selectedFile="test.md"
content="Test content"
handleContentChange={mockHandleContentChange}
handleSave={mockHandleSave}
handleFileSelect={mockHandleFileSelect}
/>
</TestWrapper>
);
expect(getByText('No workspace selected.')).toBeInTheDocument();
});
it('shows no file message when no file selected', () => {
const { getByText } = render(
<TestWrapper>
<ContentView
activeTab="source"
selectedFile={null}
content=""
handleContentChange={mockHandleContentChange}
handleSave={mockHandleSave}
handleFileSelect={mockHandleFileSelect}
/>
</TestWrapper>
);
expect(getByText('No file selected.')).toBeInTheDocument();
});
it('renders editor when activeTab is source', () => {
const { getByTestId } = render(
<TestWrapper>
<ContentView
activeTab="source"
selectedFile="test.md"
content="Test content"
handleContentChange={mockHandleContentChange}
handleSave={mockHandleSave}
handleFileSelect={mockHandleFileSelect}
/>
</TestWrapper>
);
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(
<TestWrapper>
<ContentView
activeTab="preview"
selectedFile="test.md"
content="# Test content"
handleContentChange={mockHandleContentChange}
handleSave={mockHandleSave}
handleFileSelect={mockHandleFileSelect}
/>
</TestWrapper>
);
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(
<TestWrapper>
<ContentView
activeTab="source"
selectedFile="image.png"
content=""
handleContentChange={mockHandleContentChange}
handleSave={mockHandleSave}
handleFileSelect={mockHandleFileSelect}
/>
</TestWrapper>
);
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(
<TestWrapper>
<ContentView
activeTab="preview"
selectedFile="image.png"
content=""
handleContentChange={mockHandleContentChange}
handleSave={mockHandleSave}
handleFileSelect={mockHandleFileSelect}
/>
</TestWrapper>
);
// 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();
});
});

View File

@@ -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 }) => (
<MantineProvider defaultColorScheme="light">{children}</MantineProvider>
);
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(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
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(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
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(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
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(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
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(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
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(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
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(
<MarkdownPreview
content={content}
handleFileSelect={mockHandleFileSelect}
/>
);
// Should render empty content when no workspace
const markdownPreview = screen.getByTestId('markdown-preview');
expect(markdownPreview).toBeEmptyDOMElement();
});
it('handles empty content gracefully', async () => {
render(
<MarkdownPreview content="" handleFileSelect={mockHandleFileSelect} />
);
await waitFor(() => {
const markdownPreview = screen.getByTestId('markdown-preview');
expect(markdownPreview).toBeInTheDocument();
});
});
it('updates content when markdown changes', async () => {
const { rerender } = render(
<MarkdownPreview
content="# First Content"
handleFileSelect={mockHandleFileSelect}
/>
);
await waitFor(() => {
expect(screen.getByText('First Content')).toBeInTheDocument();
});
rerender(
<TestWrapper>
<MarkdownPreview
content="# Updated Content"
handleFileSelect={mockHandleFileSelect}
/>
</TestWrapper>
);
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(
<MarkdownPreview
content={problematicContent}
handleFileSelect={mockHandleFileSelect}
/>
);
// 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(
<MarkdownPreview
content={encodedContent}
handleFileSelect={mockHandleFileSelect}
/>
);
await waitFor(() => {
const link = screen.getByText('Test Link');
fireEvent.click(link);
expect(mockHandleFileSelect).toHaveBeenCalledWith(
'test file with spaces.md'
);
});
});
});

View File

@@ -135,7 +135,11 @@ const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
void processContent(); void processContent();
}, [content, processor, currentWorkspace]); }, [content, processor, currentWorkspace]);
return <div className="markdown-preview">{processedContent}</div>; return (
<div className="markdown-preview" data-testid="markdown-preview">
{processedContent}
</div>
);
}; };
export default MarkdownPreview; export default MarkdownPreview;