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
Binary file modified .gitignore
Binary file not shown.
1 change: 1 addition & 0 deletions src/contexts/MarkdownEditorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const MarkdownEditorProvider = ({ children }: { children: ReactNode }) =>
</MarkdownEditorContext.Provider>
);
};
// eslint-disable-next-line react-refresh/only-export-components

Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The eslint-disable-next-line react-refresh/only-export-components comment has been placed after the closing brace of MarkdownEditorProvider (line 35), but it is followed by a blank line before the function it is intended to suppress (useMarkdownEditorContext at line 37). ESLint disable-next-line comments apply to the immediately following non-blank line, so the blank line between the comment and the function means the directive may not work as intended and could suppress a warning on the wrong line. The comment should be placed directly above useMarkdownEditorContext with no blank line in between.

Suggested change

Copilot uses AI. Check for mistakes.
export const useMarkdownEditorContext = () => {
const context = useContext(MarkdownEditorContext);
Expand Down
42 changes: 23 additions & 19 deletions src/tests/components/SettingsModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,33 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen } from '@testing-library/react';
import SettingsModal from '../../components/SettingsModal';

// Mock the store - use inline functions to avoid hoisting issues
interface StoreState {
isSettingsOpen: boolean;
setSettingsOpen: (value: boolean) => void;
showLineNumbers: boolean;
setShowLineNumbers: (value: boolean) => void;
textColor: string;
backgroundColor: string;
toggleDarkMode: () => void;
}
Comment on lines +5 to +13
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StoreState type used for the mocked store doesn't match the real store API: setSettingsOpen and setShowLineNumbers are declared as () => void, but the actual store signatures accept a boolean argument. Updating these to (value: boolean) => void will keep the mock aligned with production types and prevents the test from silently accepting incorrect selector usage.

Copilot uses AI. Check for mistakes.

// Update the mock
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 15 reads // Update the mock, which is a process-oriented note (describing an action taken during development) rather than explanatory documentation. It doesn't convey why the mock is structured this way. The original comment // Mock the store - use inline functions to avoid hoisting issues was more informative. This should be updated to describe the intent of the mock.

Suggested change
// Update the mock
// Mock the Zustand store selector with a fixed settings state so SettingsModal can be tested in isolation

Copilot uses AI. Check for mistakes.
vi.mock('../../store/store', () => {
return {
default: vi.fn((selector) => selector({
isSettingsOpen: true,
setSettingsOpen: vi.fn(),
showLineNumbers: true,
setShowLineNumbers: vi.fn(),
textColor: '#121212',
backgroundColor: '#ffffff',
toggleDarkMode: vi.fn(),
})),
default: vi.fn((selector: (state: StoreState) => unknown) =>
selector({
isSettingsOpen: true,
setSettingsOpen: vi.fn(),
showLineNumbers: true,
setShowLineNumbers: vi.fn(),
textColor: '#121212',
backgroundColor: '#ffffff',
toggleDarkMode: vi.fn(),
})
),
};
});

// Mock react-dark-mode-toggle
vi.mock('react-dark-mode-toggle', () => ({
default: ({ checked, onChange }: { checked: boolean; onChange: () => void }) => (
<button
Expand All @@ -37,49 +48,42 @@ describe('SettingsModal', () => {

it('renders the modal with title when open', () => {
render(<SettingsModal />);

expect(screen.getByText('Settings')).toBeInTheDocument();
});

it('renders the Dark Mode setting', () => {
render(<SettingsModal />);

expect(screen.getByText('Dark Mode')).toBeInTheDocument();
expect(screen.getByText('Toggle between light and dark theme')).toBeInTheDocument();
});

it('renders the Show Line Numbers setting', () => {
render(<SettingsModal />);

expect(screen.getByText('Show Line Numbers')).toBeInTheDocument();
expect(screen.getByText('Display line numbers in code editors')).toBeInTheDocument();
});

it('renders the dark mode toggle button', () => {
render(<SettingsModal />);

const toggle = screen.getByTestId('dark-mode-toggle');
expect(toggle).toBeInTheDocument();
});

it('renders the line numbers toggle switch', () => {
render(<SettingsModal />);

const toggle = screen.getByRole('switch', { name: /toggle line numbers/i });
expect(toggle).toBeInTheDocument();
});

it('line numbers toggle is checked when showLineNumbers is true', () => {
render(<SettingsModal />);

const toggle = screen.getByRole('switch', { name: /toggle line numbers/i });
expect(toggle).toBeChecked();
});

it('renders divider between settings', () => {
render(<SettingsModal />);

const divider = document.querySelector('hr');
expect(divider).toBeInTheDocument();
});
});
});
47 changes: 25 additions & 22 deletions src/utils/helpers/errorUtils.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
// Helper function to extract meaningful error message from complex error objects
interface NestedError {
error?: {
message?: string;
};
}

interface CommonErrorStructure {
error?: string | {
message?: string;
};
detail?: string;
message?: string;
}

export const extractErrorMessage = (error: Error | unknown): string => {
if (!error) return 'An unknown error occurred';

let errorMessage = error instanceof Error ? error.message : String(error);
const errorMessage = error instanceof Error ? error.message : String(error);

// Try to extract JSON from the message (might be prefixed with error type/code)
let jsonMatch = errorMessage.match(/\{.*\}/s);
const jsonMatch = errorMessage.match(/\{.*\}/s);
if (jsonMatch) {
try {
const parsed = JSON.parse(jsonMatch[0]);
const parsed = JSON.parse(jsonMatch[0]) as CommonErrorStructure;

// Handle Google error structure: {"error":{"message":"..."}}
if (parsed.error) {
if (typeof parsed.error === 'object') {
// Handle nested error with message
if (parsed.error.message) {
if (typeof parsed.error.message === 'string') {
try {
const inner = JSON.parse(parsed.error.message);
const inner = JSON.parse(parsed.error.message) as NestedError;
if (inner?.error?.message) {
return inner.error.message;
}
Expand All @@ -26,63 +36,56 @@ export const extractErrorMessage = (error: Error | unknown): string => {
}
}
}
} else if (typeof parsed.error === 'string') {

Comment on lines +39 to +40
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the first if (parsed.error) block (lines 39–41), a new else if (typeof parsed.error === 'string') branch was added but its body is completely empty. The actual string-error handling then happens again immediately below in a separate if (typeof parsed.error === 'string') check (line 43). This empty else if block is dead/unreachable code and adds confusion. It should be removed entirely, keeping only the single if (typeof parsed.error === 'string') check that actually returns the value.

Suggested change
} else if (typeof parsed.error === 'string') {

Copilot uses AI. Check for mistakes.
}
// Handle error as string
if (typeof parsed.error === 'string') {
return parsed.error;
}
}

// Handle Mistral error structure: {"detail":"..."}
if (parsed.detail) {
return parsed.detail;
}

// Handle direct error message
if (parsed.message) {
return parsed.message;
}
} catch {
// JSON parsing failed, continue
// Ignore
}
}

// Try to parse the entire message as JSON (for cleanly formatted JSON errors)
try {
const parsed = JSON.parse(errorMessage);
const parsed = JSON.parse(errorMessage) as CommonErrorStructure;

// Handle nested error structures
if (parsed.error) {
if (typeof parsed.error === 'object') {
// Handle Google nested structure
if (parsed.error.message) {
// Check if the inner message is also JSON
try {
const innerParsed = JSON.parse(parsed.error.message);
const innerParsed = JSON.parse(parsed.error.message) as NestedError;
if (innerParsed.error && innerParsed.error.message) {
return innerParsed.error.message;
}
} catch {
return parsed.error.message;
}
}
}
if (typeof parsed.error === 'string') {
} else if (typeof parsed.error === 'string') {
return parsed.error;
}
}

// Handle Mistral error structure: {"detail":"..."}
if (parsed.detail) {
return parsed.detail;
}

// Handle direct error message
if (parsed.message) {
return parsed.message;
}
} catch {
// Not JSON, return as is
// Ignore
}

return errorMessage;
Expand Down
141 changes: 81 additions & 60 deletions src/utils/testing/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ afterEach(() => {
cleanup();
});

// Mock getComputedStyle for Ant Design components that use scroll locking
// jsdom doesn't fully support getComputedStyle with pseudo-elements
// rc-util's getScrollBarSize calls .match() on style properties
const originalGetComputedStyle = window.getComputedStyle;
window.getComputedStyle = (elt: Element, pseudoElt?: string | null) => {
if (pseudoElt) {
// Return a mock CSSStyleDeclaration with string properties for .match() calls
return {
width: '0px',
height: '0px',
Expand All @@ -30,62 +26,87 @@ Object.defineProperty(window, "matchMedia", {
matches: false,
media: query,
onchange: null,
addListener: () => {
// Mock implementation for tests
},
removeListener: () => {
// Mock implementation for tests
},
addEventListener: () => {
// Mock implementation for tests
},
removeEventListener: () => {
// Mock implementation for tests
},
dispatchEvent: () => {
return false;
},
addListener: () => { /* mock */ },
removeListener: () => { /* mock */ },
addEventListener: () => { /* mock */ },
removeEventListener: () => { /* mock */ },
dispatchEvent: () => { return false; },
}),
});

// Mock HTMLCanvasElement.getContext for lottie-web library
// jsdom doesn't implement canvas 2D context
// @ts-expect-error: Mock implementation has simplified types
HTMLCanvasElement.prototype.getContext = ((originalGetContext) => {
return function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown
) {
if (contextId === '2d') {
return {
fillStyle: '',
fillRect: () => {},
clearRect: () => {},
getImageData: () => ({ data: [] }),
putImageData: () => {},
createImageData: () => ({ data: [] }),
setTransform: () => {},
drawImage: () => {},
save: () => {},
restore: () => {},
beginPath: () => {},
moveTo: () => {},
lineTo: () => {},
closePath: () => {},
stroke: () => {},
fill: () => {},
translate: () => {},
scale: () => {},
rotate: () => {},
arc: () => {},
measureText: () => ({ width: 0 }),
transform: () => {},
rect: () => {},
clip: () => {},
canvas: this,
} as unknown as CanvasRenderingContext2D;
}
return originalGetContext.call(this, contextId as any, options);
};
})(HTMLCanvasElement.prototype.getContext);
interface MockCanvasContext {
fillStyle: string;
fillRect: () => void;
clearRect: () => void;
getImageData: () => { data: number[] };
putImageData: () => void;
createImageData: () => { data: number[] };
setTransform: () => void;
drawImage: () => void;
save: () => void;
restore: () => void;
beginPath: () => void;
moveTo: () => void;
lineTo: () => void;
closePath: () => void;
stroke: () => void;
fill: () => void;
translate: () => void;
scale: () => void;
rotate: () => void;
arc: () => void;
measureText: () => { width: number };
transform: () => void;
rect: () => void;
clip: () => void;
canvas: HTMLCanvasElement;
}

// capture original method so we can delegate for non-2d calls
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comments explaining the purpose of important code blocks (e.g., "Mock getComputedStyle for Ant Design components...", "Mock HTMLCanvasElement.getContext for lottie-web library...", "Return a mock CSSStyleDeclaration with string properties for .match() calls") were removed from setup.ts. These comments were valuable documentation explaining why the mocks are needed and what specific libraries/behaviors they address. Similarly, explanatory comments describing the various error structures handled in errorUtils.ts (e.g., "Handle Google error structure: ...", "Handle Mistral error structure: ...") were removed. These comments made the code self-documenting and easier to maintain.

Copilot uses AI. Check for mistakes.
const originalGetContext = HTMLCanvasElement.prototype.getContext as (
this: HTMLCanvasElement,
contextId: string,
options?: unknown
) => RenderingContext | null;

HTMLCanvasElement.prototype.getContext = function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown
) {
if (contextId === '2d') {
const mockContext: MockCanvasContext = {
fillStyle: '',
fillRect: () => { /* mock */ },
clearRect: () => { /* mock */ },
getImageData: () => ({ data: [] }),
putImageData: () => { /* mock */ },
createImageData: () => ({ data: [] }),
setTransform: () => { /* mock */ },
drawImage: () => { /* mock */ },
save: () => { /* mock */ },
restore: () => { /* mock */ },
beginPath: () => { /* mock */ },
moveTo: () => { /* mock */ },
lineTo: () => { /* mock */ },
closePath: () => { /* mock */ },
stroke: () => { /* mock */ },
fill: () => { /* mock */ },
translate: () => { /* mock */ },
scale: () => { /* mock */ },
rotate: () => { /* mock */ },
arc: () => { /* mock */ },
measureText: () => ({ width: 0 }),
transform: () => { /* mock */ },
rect: () => { /* mock */ },
clip: () => { /* mock */ },
canvas: this,
};
// cast through unknown since the mock only implements a subset of methods
return mockContext as unknown as CanvasRenderingContext2D;
}

// for other context types, delegate to original
// for other context types, delegate to original
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a duplicate comment on two consecutive lines in the getContext mock implementation. Line 109 reads // for other context types, delegate to original and line 110 is an exact duplicate of the same comment. One of them should be removed.

Suggested change
// for other context types, delegate to original

Copilot uses AI. Check for mistakes.
return originalGetContext.call(this, contextId, options);
} as unknown as typeof HTMLCanvasElement.prototype.getContext;
Loading