Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions __mocks__/rehype-raw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Mock for rehype-raw plugin
*
* rehype-raw is an ES module that causes issues with Jest's CommonJS environment.
* This mock provides a no-op transformer that passes the syntax tree through unchanged.
*
* In tests, we don't need actual HTML parsing since we're testing component behavior,
* not the markdown rendering itself.
*/
module.exports = function rehypeRaw() {
return function transformer(tree) {
return tree;
};
};

module.exports.default = module.exports;
16 changes: 16 additions & 0 deletions __mocks__/remark-gfm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Mock for remark-gfm (GitHub Flavored Markdown) plugin
*
* remark-gfm is an ES module that causes issues with Jest's CommonJS environment.
* This mock provides a no-op transformer that passes the syntax tree through unchanged.
*
* In tests, we don't need actual GFM parsing (tables, strikethrough, etc.) since
* we're testing component behavior, not markdown rendering.
*/
module.exports = function remarkGfm() {
return function transformer(tree) {
return tree;
};
};

module.exports.default = module.exports;
16 changes: 16 additions & 0 deletions __mocks__/remark-math.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Mock for remark-math plugin
*
* remark-math is an ES module that causes issues with Jest's CommonJS environment.
* This mock provides a no-op transformer that passes the syntax tree through unchanged.
*
* In tests, we don't need actual LaTeX math parsing since we're testing component
* behavior, not mathematical equation rendering.
*/
module.exports = function remarkMath() {
return function transformer(tree) {
return tree;
};
};

module.exports.default = module.exports;
112 changes: 112 additions & 0 deletions __tests__/components/Chat/ChatMessage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ChatMessage } from '@/components/Chat/ChatMessage';
import HomeContext from '@/pages/api/home/home.context';
import { Message } from '@/types/chat';

// Mock the HtmlFileRenderer to check if it's being rendered
jest.mock('@/components/Chat/HtmlFileRenderer', () => ({
HtmlFileRenderer: jest.fn(() => <div data-testid="html-file-renderer" />),
}));

// Mock the Avatar component
jest.mock('@/components/Avatar/BotAvatar', () => ({
BotAvatar: jest.fn(() => <div data-testid="bot-avatar" />),
}));

describe('ChatMessage', () => {
const mockHomeContextValue = {
state: {
selectedConversation: {
messages: [],
},
conversations: [],
messageIsStreaming: false,
},
dispatch: jest.fn(),
};

const renderWithMessage = (message: Message) => {
return render(
<HomeContext.Provider value={mockHomeContextValue as any}>
<ChatMessage message={message} messageIndex={0} />
</HomeContext.Provider>
);
};

it('should render HtmlFileRenderer for an assistant message with an HTML file link', () => {
const message: Message = {
id: "1",
role: 'assistant',
content: 'Here is your plot: <a href="file:///path/to/plot.html">Plot</a>',
};
renderWithMessage(message);

expect(screen.getByTestId('html-file-renderer')).toBeInTheDocument();
});

it('should remove the HTML file link from the displayed message content', () => {
const message: Message = {
id: "1",
role: 'assistant',
content: 'Here is your plot: <a href="file:///path/to/plot.html">Plot</a>',
};
renderWithMessage(message);

// Verify the link is removed but surrounding text is preserved
expect(screen.getByText(/Here is your plot/i)).toBeInTheDocument();
// The HtmlFileRenderer should be shown instead of the raw link
expect(screen.getByTestId('html-file-renderer')).toBeInTheDocument();
});

it('should not render HtmlFileRenderer for a user message, even with a link', () => {
const message: Message = {
id: "1",
role: 'user',
content: 'Can you show me a plot from <a href="file:///path/to/plot.html">Plot</a>?',
};
renderWithMessage(message);

expect(screen.queryByTestId('html-file-renderer')).not.toBeInTheDocument();
// Check that the content is rendered as is for the user message
expect(screen.getByText(/Can you show me a plot from/)).toBeInTheDocument();
});

it('should not render HtmlFileRenderer for an assistant message without a link', () => {
const message: Message = {
id: "1",
role: 'assistant',
content: 'Hello! How can I help you today?',
};
renderWithMessage(message);

expect(screen.queryByTestId('html-file-renderer')).not.toBeInTheDocument();
expect(screen.getByText('Hello! How can I help you today?')).toBeInTheDocument();
});

it('should render HtmlFileRenderer for inline HTML content', () => {
const message: Message = {
id: "1",
role: 'assistant',
content: 'Here is an inline plot: <html><body><h1>My Plot</h1></body></html>',
};
renderWithMessage(message);

expect(screen.getByTestId('html-file-renderer')).toBeInTheDocument();
expect(screen.getByText(/Here is an inline plot/i)).toBeInTheDocument();
});

it('should handle multiple HTML files in one message', () => {
const message: Message = {
id: "1",
role: 'assistant',
content: 'First: <a href="file:///plot1.html">Plot 1</a> and second: [Plot 2](file:///plot2.html)',
};
renderWithMessage(message);

// Should render multiple HtmlFileRenderer components (mocked)
const renderers = screen.getAllByTestId('html-file-renderer');
expect(renderers.length).toBeGreaterThanOrEqual(2);
});
});
147 changes: 147 additions & 0 deletions __tests__/components/Chat/HtmlFileRenderer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { HtmlFileRenderer } from '@/components/Chat/HtmlFileRenderer';

// Mock fetch
global.fetch = jest.fn();

// Mock navigator.clipboard
Object.defineProperty(navigator, 'clipboard', {
value: {
writeText: jest.fn().mockResolvedValue(undefined),
},
writable: true,
});

describe('HtmlFileRenderer', () => {
beforeEach(() => {
(fetch as jest.Mock).mockClear();
(navigator.clipboard.writeText as jest.Mock).mockClear();
});

it('should render loading state initially, then render the plot when file exists', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true, // For checkFileExists
});
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
text: () => Promise.resolve('<html><body><h1>Test Plot</h1></body></html>'), // For loadHtmlContent
});

render(<HtmlFileRenderer filePath="file:///path/to/plot.html" title="My Plot" />);

// It should initially show a loading skeleton, but that's hard to test without more specific selectors.
// We'll wait for the content to appear.

await waitFor(() => {
expect(screen.getByTitle('My Plot')).toBeInTheDocument();
});

const iframe = screen.getByTitle('My Plot');
expect(iframe).toHaveAttribute('srcDoc', '<html><body><h1>Test Plot</h1></body></html>');
});

it('should render nothing if the file does not exist', async () => {
(fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 404,
});

const { container } = render(<HtmlFileRenderer filePath="file:///path/to/nonexistent.html" />);

await waitFor(() => {
expect(container.firstChild).toBeNull();
});
});

it('should display an error message if fetching content fails', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({ ok: true }); // file exists check
(fetch as jest.Mock).mockRejectedValueOnce(new Error('Network error')); // content fetch fails

render(<HtmlFileRenderer filePath="file:///path/to/plot.html" />);

await waitFor(() => {
expect(screen.getByText(/Could not load plot inline/i)).toBeInTheDocument();
expect(screen.getByText(/Failed to load HTML file: Network error/i)).toBeInTheDocument();
});
});

it('should render inline HTML content directly without fetching', async () => {
const inlineContent = '<html><body><h2>Inline Content</h2></body></html>';
render(<HtmlFileRenderer filePath="inline-1" isInlineHtml={true} htmlContent={inlineContent} />);

await waitFor(() => {
expect(screen.getByTitle('Inline HTML Content')).toBeInTheDocument();
});

const iframe = screen.getByTitle('Inline HTML Content');
expect(iframe).toHaveAttribute('srcDoc', inlineContent);
expect(fetch).not.toHaveBeenCalled();
});

it('should toggle the visibility of the plot when show/hide button is clicked', async () => {
(fetch as jest.Mock).mockResolvedValue({
ok: true,
text: () => Promise.resolve('<html></html>'),
});

render(<HtmlFileRenderer filePath="file:///path/to/plot.html" />);

await waitFor(() => {
expect(screen.getByTitle('HTML Content')).toBeInTheDocument();
});

const hideButton = screen.getByRole('button', { name: /Hide Plot/i });
fireEvent.click(hideButton);

await waitFor(() => {
expect(screen.queryByTitle('HTML Content')).not.toBeInTheDocument();
});

const showButton = screen.getByRole('button', { name: /Show Plot/i });
fireEvent.click(showButton);

await waitFor(() => {
expect(screen.getByTitle('HTML Content')).toBeInTheDocument();
});
});

it('should copy file path to clipboard', async () => {
(fetch as jest.Mock).mockResolvedValue({ ok: true });
window.alert = jest.fn();

render(<HtmlFileRenderer filePath="file:///path/to/plot.html" />);

await waitFor(() => {
expect(screen.getByTitle('Copy file path')).toBeInTheDocument();
});

const copyButton = screen.getByTitle('Copy file path');
fireEvent.click(copyButton);

await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('/path/to/plot.html');
expect(window.alert).toHaveBeenCalledWith('File path copied to clipboard! Paste it into your browser address bar to view the plot.');
});
});

it('should copy inline html to clipboard', async () => {
const inlineContent = '<html><body><h2>Inline Content</h2></body></html>';
window.alert = jest.fn();

render(<HtmlFileRenderer filePath="inline-1" isInlineHtml={true} htmlContent={inlineContent} />);

await waitFor(() => {
expect(screen.getByTitle('Copy HTML content')).toBeInTheDocument();
});

const copyButton = screen.getByTitle('Copy HTML content');
fireEvent.click(copyButton);

await waitFor(() => {
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(inlineContent);
expect(window.alert).toHaveBeenCalledWith('HTML content copied to clipboard!');
});
});
});
Loading