From e088923afc2d49237c6751d89ae3a2b3b9374af3 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 14 Oct 2026 00:32:05 +0000 Subject: [PATCH 1/3] fix(workspace): refetch failed route reads and contain editor width Refs sass-maker/saas-maker#244, sass-maker/saas-maker#242 and sass-maker/saas-maker#241. Synthetic local acceptance only; production identity and device gates remain separate. --- __tests__/route-error-retry.test.tsx | 38 ++++++++++++++++++++++++ src/app/cover-letter/[jobId]/error.tsx | 6 ++-- src/app/dashboard/error.tsx | 6 ++-- src/app/editor/[id]/error.tsx | 6 ++-- src/app/error.tsx | 6 ++-- src/app/global-error.tsx | 6 ++-- src/app/interview-prep/[jobId]/error.tsx | 6 ++-- src/app/tailor/[jobId]/error.tsx | 6 ++-- src/components/workspace-interior.tsx | 2 +- 9 files changed, 60 insertions(+), 22 deletions(-) create mode 100644 __tests__/route-error-retry.test.tsx diff --git a/__tests__/route-error-retry.test.tsx b/__tests__/route-error-retry.test.tsx new file mode 100644 index 0000000..73ce8a6 --- /dev/null +++ b/__tests__/route-error-retry.test.tsx @@ -0,0 +1,38 @@ +import { act, createElement } from 'react'; +import { createRoot } from 'react-dom/client'; +import { afterEach, expect, it, vi } from 'vitest'; +import RootError from '@/app/error'; +import GlobalError from '@/app/global-error'; +import DashboardError from '@/app/dashboard/error'; +import EditorError from '@/app/editor/[id]/error'; +import TailorError from '@/app/tailor/[jobId]/error'; +import CoverLetterError from '@/app/cover-letter/[jobId]/error'; +import InterviewError from '@/app/interview-prep/[jobId]/error'; + +vi.mock('@/lib/foundry-monitoring', () => ({ captureError: vi.fn() })); +afterEach(() => vi.restoreAllMocks()); +for (const [name, Boundary] of [ + ['root', RootError], + ['global', GlobalError], + ['dashboard', DashboardError], + ['editor', EditorError], + ['tailor', TailorError], + ['cover letter', CoverLetterError], + ['interview', InterviewError], +] as const) { + it(`${name} retries the failed server segment instead of merely clearing its cached error`, async () => { + vi.spyOn(console, 'error').mockImplementation(() => {}); + const retry = vi.fn(); + const reset = vi.fn(); + const node = document.createElement('div'); + document.body.append(node); + const root = createRoot(node); + const props = { error: new Error('Synthetic read failure'), retry, reset }; + await act(async () => root.render(createElement(Boundary, props))); + await act(async () => node.querySelector('button')!.click()); + expect(retry).toHaveBeenCalledOnce(); + expect(reset).not.toHaveBeenCalled(); + await act(async () => root.unmount()); + node.remove(); + }); +} diff --git a/src/app/cover-letter/[jobId]/error.tsx b/src/app/cover-letter/[jobId]/error.tsx index 8e5be45..fdf67cd 100644 --- a/src/app/cover-letter/[jobId]/error.tsx +++ b/src/app/cover-letter/[jobId]/error.tsx @@ -7,10 +7,10 @@ import { captureError } from '@/lib/foundry-monitoring'; export default function CoverLetterError({ error, - reset, + retry, }: { error: Error & { digest?: string }; - reset: () => void; + retry: () => void; }) { useEffect(() => { console.error(error); @@ -25,7 +25,7 @@ export default function CoverLetterError({ Something went wrong while preparing this page. Any saved drafts are safe — try again.

- diff --git a/src/app/dashboard/error.tsx b/src/app/dashboard/error.tsx index 9beac22..eccb2f3 100644 --- a/src/app/dashboard/error.tsx +++ b/src/app/dashboard/error.tsx @@ -6,10 +6,10 @@ import { captureError } from '@/lib/foundry-monitoring'; export default function DashboardError({ error, - reset, + retry, }: { error: Error & { digest?: string }; - reset: () => void; + retry: () => void; }) { useEffect(() => { console.error(error); @@ -23,7 +23,7 @@ export default function DashboardError({

Something went wrong while loading your resumes and jobs. Your data is safe — try again.

- {error.digest ?

Reference: {error.digest}

: null} diff --git a/src/app/editor/[id]/error.tsx b/src/app/editor/[id]/error.tsx index 80c7ecb..b3b692a 100644 --- a/src/app/editor/[id]/error.tsx +++ b/src/app/editor/[id]/error.tsx @@ -7,10 +7,10 @@ import { captureError } from '@/lib/foundry-monitoring'; export default function EditorError({ error, - reset, + retry, }: { error: Error & { digest?: string }; - reset: () => void; + retry: () => void; }) { useEffect(() => { console.error(error); @@ -25,7 +25,7 @@ export default function EditorError({ Something went wrong while loading this resume. Your saved content is safe — try again.

- diff --git a/src/app/error.tsx b/src/app/error.tsx index c08527c..18f69a9 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -6,10 +6,10 @@ import { captureError } from '@/lib/foundry-monitoring'; export default function Error({ error, - reset, + retry, }: { error: Error & { digest?: string }; - reset: () => void; + retry: () => void; }) { useEffect(() => { // Full detail goes to the console + PostHog — never to the user. @@ -26,7 +26,7 @@ export default function Error({ happening, come back in a few minutes.

- diff --git a/src/app/tailor/[jobId]/error.tsx b/src/app/tailor/[jobId]/error.tsx index 1edba7f..9c7f2ef 100644 --- a/src/app/tailor/[jobId]/error.tsx +++ b/src/app/tailor/[jobId]/error.tsx @@ -7,10 +7,10 @@ import { captureError } from '@/lib/foundry-monitoring'; export default function TailorError({ error, - reset, + retry, }: { error: Error & { digest?: string }; - reset: () => void; + retry: () => void; }) { useEffect(() => { console.error(error); @@ -26,7 +26,7 @@ export default function TailorError({ again.

- diff --git a/src/components/workspace-interior.tsx b/src/components/workspace-interior.tsx index 72482c0..0fb56c0 100644 --- a/src/components/workspace-interior.tsx +++ b/src/components/workspace-interior.tsx @@ -133,7 +133,7 @@ export function WorkspaceInterior({ - +
From 0c187666b30fdca3fa1094b5fc6810d237dff22b Mon Sep 17 00:00:00 2001 From: Sarthak Agrawal Date: Wed, 14 Oct 2026 06:56:26 +0000 Subject: [PATCH 2/3] fix: keep resume toolbar controls reachable in narrow panes Refs #40; successor to e088923 for PR #41. Preserve the existing paper width and toolbar actions while containing wrapped controls and settings. --- e2e/editor-toolbar.spec.ts | 102 +++++++++++++++++++++++++++++++ src/components/resume-editor.tsx | 6 +- 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 e2e/editor-toolbar.spec.ts diff --git a/e2e/editor-toolbar.spec.ts b/e2e/editor-toolbar.spec.ts new file mode 100644 index 0000000..c2e5045 --- /dev/null +++ b/e2e/editor-toolbar.spec.ts @@ -0,0 +1,102 @@ +import { expect, test } from '@playwright/test'; + +for (const width of [390, 768, 1440]) { + test(`initial editor toolbar hit targets and keyboard access at ${width}`, async ({ + context, + page, + }, testInfo) => { + await page.setViewportSize({ width, height: 900 }); + await context.addInitScript(() => { + localStorage.setItem( + 'rt-resumes', + JSON.stringify([ + { + id: 'synthetic-toolbar', + name: 'Synthetic Resume', + source: '# Ada Example\n\n## Experience\n- Built accessible TypeScript screens.\n', + created_at: 1767225600, + updated_at: 1767225600, + }, + ]) + ); + }); + await context.routeWebSocket('**/*', (socket) => socket.close()); + await context.route('**/*', (route) => { + const url = new URL(route.request().url()); + if (url.origin !== new URL(testInfo.project.use.baseURL!).origin) return route.abort(); + if (url.pathname === '/api/auth/get-session') return route.fulfill({ json: null }); + if (url.pathname.startsWith('/api/')) + return route.fulfill({ status: 503, json: { error: 'Unseeded synthetic operation' } }); + return route.continue(); + }); + await page.goto('/editor/synthetic-toolbar'); + await expect(page.locator('.cm-editor')).toBeVisible(); + await page.evaluate(() => document.fonts.ready); + const toolbar = page.locator('.product-editor-toolbar'); + const targets = [ + toolbar.getByTitle('Back to Resume Builder'), + toolbar.getByTitle('Click to rename'), + toolbar.getByTitle('Format settings'), + toolbar.locator('summary').filter({ hasText: 'Export resume' }), + toolbar.getByRole('button', { name: 'Save', exact: true }), + ]; + const initialScroll = await toolbar.evaluate((element) => { + const pane = element.parentElement!; + return { document: window.scrollX, pane: pane.scrollLeft }; + }); + // No locator.click(), focus() or scrollIntoView() before these initial hit tests. + for (const target of targets) { + expect( + await target.evaluate((element) => { + const box = element.getBoundingClientRect(); + const first = document.elementsFromPoint( + box.x + box.width / 2, + box.y + box.height / 2 + )[0]; + return ( + box.width > 0 && + box.height > 0 && + box.x >= 0 && + box.right <= innerWidth && + first != null && + (first === element || element.contains(first)) + ); + }) + ).toBe(true); + } + expect( + await toolbar.evaluate((element) => ({ + document: window.scrollX, + pane: element.parentElement!.scrollLeft, + })) + ).toEqual(initialScroll); + expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe( + true + ); + await expect(page.locator('.resume-page').first()).toHaveCSS('width', '816px'); + await page.screenshot({ path: testInfo.outputPath('initial.png'), fullPage: true }); + const reached = new Set(); + for (let index = 0; index < 50 && reached.size < targets.length; index += 1) { + await page.keyboard.press('Tab'); + for (let n = 0; n < targets.length; n += 1) + if (await targets[n].evaluate((element) => element === document.activeElement)) + reached.add(String(n)); + } + expect(reached.size).toBe(targets.length); + // Open at its measured coordinate; pointer qualification cannot silently pan the pane. + const format = await toolbar.getByTitle('Format settings').boundingBox(); + await page.mouse.click(format!.x + format!.width / 2, format!.y + format!.height / 2); + for (const control of ['select', 'input[type=number]']) { + for (const element of await toolbar.locator(control).all()) { + expect( + await element.evaluate((node) => { + const box = node.getBoundingClientRect(); + const first = document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2); + return box.width > 0 && first != null && (first === node || node.contains(first)); + }) + ).toBe(true); + } + } + await page.screenshot({ path: testInfo.outputPath('settings.png'), fullPage: true }); + }); +} diff --git a/src/components/resume-editor.tsx b/src/components/resume-editor.tsx index 6e06dc1..67ba29e 100644 --- a/src/components/resume-editor.tsx +++ b/src/components/resume-editor.tsx @@ -416,7 +416,7 @@ export function ResumeEditor({ resumeId, initialSource, resumeName }: Props) { return (
{/* Editor pane */} -
+
{/* Toolbar */}
{saving ? 'Saving...' : ''} -
+
{showConfig && ( -
+