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.
2 changes: 1 addition & 1 deletion src/contexts/MarkdownEditorContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const MarkdownEditorProvider = ({ children }: { children: ReactNode }) =>
</MarkdownEditorContext.Provider>
);
};

// eslint-disable-next-line react-refresh/only-export-components
export const useMarkdownEditorContext = () => {
const context = useContext(MarkdownEditorContext);
if (context === undefined) {
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;
}

// Mock the Zustand store selector with a fixed settings state for isolated SettingsModal tests
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();
});
});
});
45 changes: 23 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 @@ -33,56 +43,47 @@ export const extractErrorMessage = (error: Error | unknown): string => {
}
}

// 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
137 changes: 80 additions & 57 deletions src/utils/testing/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ vi.mock("monaco-editor", () => ({
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 @@ -43,62 +42,86 @@ 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
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
return originalGetContext.call(this, contextId, options);
} as unknown as typeof HTMLCanvasElement.prototype.getContext;