Skip to content
Merged
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
18 changes: 18 additions & 0 deletions apps/desktop/src/renderer/src/App.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest';
import { formatIframeError } from './components/PreviewPane';

describe('formatIframeError', () => {
it('omits location when source or lineno is missing', () => {
expect(formatIframeError('error', 'something broke')).toBe('error: something broke');
expect(formatIframeError('error', 'something broke', 'app.js')).toBe('error: something broke');
expect(formatIframeError('error', 'something broke', undefined, 10)).toBe(
'error: something broke',
);
});

it('appends source and lineno when both are present', () => {
expect(formatIframeError('unhandledrejection', 'promise failed', 'app.js', 42)).toBe(
'unhandledrejection: promise failed (app.js:42)',
);
});
});
23 changes: 18 additions & 5 deletions apps/desktop/src/renderer/src/components/PreviewPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ export interface PreviewPaneProps {
onPickStarter: (prompt: string) => void;
}

export function formatIframeError(
kind: string,
message: string,
source?: string,
lineno?: number,
): string {
const location = source && lineno ? ` (${source}:${lineno})` : '';
return `${kind}: ${message}${location}`;
}

export function isTrustedPreviewMessageSource(
source: MessageEventSource | null,
previewWindow: Window | null | undefined,
Expand Down Expand Up @@ -46,11 +56,14 @@ export function PreviewPane({ onPickStarter }: PreviewPaneProps) {
}

if (isIframeErrorMessage(event.data)) {
const location =
event.data.source && event.data.lineno
? ` (${event.data.source}:${event.data.lineno})`
: '';
pushIframeError(`${event.data.kind}: ${event.data.message}${location}`);
pushIframeError(
formatIframeError(
event.data.kind,
event.data.message,
event.data.source,
event.data.lineno,
),
);
}
}

Expand Down
97 changes: 97 additions & 0 deletions apps/desktop/src/renderer/src/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { OnboardingState } from '@open-codesign/shared';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useCodesignStore } from './store';

const READY_CONFIG: OnboardingState = {
hasKey: true,
provider: 'anthropic',
modelPrimary: 'claude-sonnet-4-6',
modelFast: 'claude-haiku-3',
baseUrl: null,
designSystem: null,
};

const initialState = useCodesignStore.getState();

function resetStore() {
useCodesignStore.setState({
...initialState,
messages: [],
previewHtml: null,
isGenerating: false,
errorMessage: null,
lastError: null,
config: READY_CONFIG,
configLoaded: true,
toastMessage: null,
iframeErrors: [],
toasts: [],
});
}

beforeEach(() => {
resetStore();
});

afterEach(() => {
vi.unstubAllGlobals();
});

describe('useCodesignStore iframe error handling', () => {
it('clears stale iframe errors when starting a new generation', async () => {
let resolveGenerate: ((value: unknown) => void) | undefined;
const generate = vi.fn(
() =>
new Promise((resolve) => {
resolveGenerate = resolve;
}),
);

vi.stubGlobal('window', {
codesign: {
generate,
},
});

useCodesignStore.setState({ iframeErrors: ['old iframe error'] });

const sendPromise = useCodesignStore.getState().sendPrompt({ prompt: 'make a landing page' });

expect(useCodesignStore.getState().iframeErrors).toEqual([]);
expect(useCodesignStore.getState().isGenerating).toBe(true);

resolveGenerate?.({
artifacts: [{ content: '<html></html>' }],
message: 'Done.',
});
await sendPromise;

expect(generate).toHaveBeenCalledOnce();
});

it('deduplicates consecutive identical iframe errors', () => {
const { pushIframeError } = useCodesignStore.getState();

pushIframeError('first');
pushIframeError('first'); // duplicate — should be skipped
pushIframeError('second');
pushIframeError('second'); // duplicate — should be skipped
pushIframeError('third');

expect(useCodesignStore.getState().iframeErrors).toEqual(['first', 'second', 'third']);
});

it('caps iframeErrors at 50 entries and drops the oldest when exceeded', () => {
const { pushIframeError } = useCodesignStore.getState();

for (let i = 0; i < 55; i++) {
pushIframeError(`error-${i}`);
}

const errors = useCodesignStore.getState().iframeErrors;
expect(errors).toHaveLength(50);
// oldest (0-4) should have been shifted out; newest (5-54) remain
expect(errors[0]).toBe('error-5');
expect(errors[49]).toBe('error-54');
});
});
9 changes: 6 additions & 3 deletions apps/desktop/src/renderer/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ export const useCodesignStore = create<CodesignState>((set, get) => ({
},

pushIframeError(message) {
set((s) => ({
iframeErrors: [...s.iframeErrors.slice(-9), message],
}));
set((s) => {
const last = s.iframeErrors[s.iframeErrors.length - 1];
if (last === message) return {};
const next = [...s.iframeErrors, message];
return { iframeErrors: next.length > 50 ? next.slice(1) : next };
});
},

async loadConfig() {
Expand Down
Loading