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
38 changes: 38 additions & 0 deletions __tests__/route-error-retry.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
}
104 changes: 104 additions & 0 deletions e2e/editor-toolbar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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) => {
test.setTimeout(90_000);
await page.setViewportSize({ width, height: 900 });
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 });
return route.continue();
});
await page.goto('/resume-builder');
await expect(page.getByRole('button', { name: '+ New Resume', exact: true })).toBeEnabled({
timeout: 30_000,
});
await page.evaluate(() =>
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 page.goto('/editor/synthetic-toolbar');
await expect(page.locator('.cm-editor')).toBeVisible({ timeout: 30_000 });
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<string>();
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 });
});
}
6 changes: 3 additions & 3 deletions src/app/cover-letter/[jobId]/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -25,7 +25,7 @@ export default function CoverLetterError({
Something went wrong while preparing this page. Any saved drafts are safe — try again.
</p>
<div className="flex gap-3 justify-center">
<button onClick={reset} className="px-4 py-2 rounded border hover:opacity-80">
<button onClick={retry} className="px-4 py-2 rounded border hover:opacity-80">
Try again
</button>
<Link href="/dashboard" className="px-4 py-2 rounded border hover:opacity-80">
Expand Down
6 changes: 3 additions & 3 deletions src/app/dashboard/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -23,7 +23,7 @@ export default function DashboardError({
<p className="text-sm opacity-70 mb-6">
Something went wrong while loading your resumes and jobs. Your data is safe — try again.
</p>
<button onClick={reset} className="px-4 py-2 rounded border hover:opacity-80">
<button onClick={retry} className="px-4 py-2 rounded border hover:opacity-80">
Try again
</button>
{error.digest ? <p className="mt-6 text-xs opacity-40">Reference: {error.digest}</p> : null}
Expand Down
6 changes: 3 additions & 3 deletions src/app/editor/[id]/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -25,7 +25,7 @@ export default function EditorError({
Something went wrong while loading this resume. Your saved content is safe — try again.
</p>
<div className="flex gap-3 justify-center">
<button onClick={reset} className="px-4 py-2 rounded border hover:opacity-80">
<button onClick={retry} className="px-4 py-2 rounded border hover:opacity-80">
Try again
</button>
<Link href="/dashboard" className="px-4 py-2 rounded border hover:opacity-80">
Expand Down
6 changes: 3 additions & 3 deletions src/app/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -26,7 +26,7 @@ export default function Error({
happening, come back in a few minutes.
</p>
<div className="flex gap-3 justify-center">
<button onClick={reset} className="px-4 py-2 rounded border hover:opacity-80">
<button onClick={retry} className="px-4 py-2 rounded border hover:opacity-80">
Try again
</button>
<button
Expand Down
6 changes: 3 additions & 3 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { captureError } from '@/lib/foundry-monitoring';
*/
export default function GlobalError({
error,
reset,
retry,
}: {
error: Error & { digest?: string };
reset: () => void;
retry: () => void;
}) {
useEffect(() => {
console.error(error);
Expand Down Expand Up @@ -44,7 +44,7 @@ export default function GlobalError({
few minutes.
</p>
<button
onClick={reset}
onClick={retry}
style={{
padding: '0.5rem 1rem',
borderRadius: 6,
Expand Down
6 changes: 3 additions & 3 deletions src/app/interview-prep/[jobId]/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { captureError } from '@/lib/foundry-monitoring';

export default function InterviewPrepError({
error,
reset,
retry,
}: {
error: Error & { digest?: string };
reset: () => void;
retry: () => void;
}) {
useEffect(() => {
console.error(error);
Expand All @@ -25,7 +25,7 @@ export default function InterviewPrepError({
Something went wrong while preparing this page. Your saved stories are safe — try again.
</p>
<div className="flex gap-3 justify-center">
<button onClick={reset} className="px-4 py-2 rounded border hover:opacity-80">
<button onClick={retry} className="px-4 py-2 rounded border hover:opacity-80">
Try again
</button>
<Link href="/dashboard" className="px-4 py-2 rounded border hover:opacity-80">
Expand Down
6 changes: 3 additions & 3 deletions src/app/tailor/[jobId]/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -26,7 +26,7 @@ export default function TailorError({
again.
</p>
<div className="flex gap-3 justify-center">
<button onClick={reset} className="px-4 py-2 rounded border hover:opacity-80">
<button onClick={retry} className="px-4 py-2 rounded border hover:opacity-80">
Try again
</button>
<Link href="/dashboard" className="px-4 py-2 rounded border hover:opacity-80">
Expand Down
6 changes: 3 additions & 3 deletions src/components/resume-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export function ResumeEditor({ resumeId, initialSource, resumeName }: Props) {
return (
<div className="flex flex-col md:flex-row w-full h-full overflow-y-auto md:overflow-hidden">
{/* Editor pane */}
<div className="w-full md:w-1/2 min-h-[50vh] md:min-h-0 flex flex-col overflow-hidden border-b md:border-b-0 md:border-r border-gray-200 print-hide">
<div className="@container/editor min-w-0 w-full md:w-1/2 min-h-[50vh] md:min-h-0 flex flex-col overflow-hidden border-b md:border-b-0 md:border-r border-gray-200 print-hide">
{/* Toolbar */}
<div className="product-editor-toolbar flex flex-wrap items-center gap-x-3 gap-y-1 px-4 py-2 border-b border-gray-200 bg-white">
<Link
Expand Down Expand Up @@ -479,7 +479,7 @@ export function ResumeEditor({ resumeId, initialSource, resumeName }: Props) {

<span className="text-xs text-gray-400">{saving ? 'Saving...' : ''}</span>

<div className="ml-auto flex items-center gap-2">
<div className="ml-auto flex min-w-0 max-w-full flex-wrap items-center justify-end gap-2">
<div className="relative" ref={configRef}>
<button
onClick={() => setShowConfig((v) => !v)}
Expand All @@ -496,7 +496,7 @@ export function ResumeEditor({ resumeId, initialSource, resumeName }: Props) {
</button>

{showConfig && (
<div className="absolute right-0 top-full mt-1 z-50 bg-white border border-gray-200 rounded-lg shadow-lg p-3 w-64">
<div className="absolute left-0 md:left-auto md:right-0 top-full mt-1 z-50 bg-white border border-gray-200 rounded-lg shadow-lg p-3 w-64 max-w-[calc(100cqw-2rem)]">
<div className="space-y-3">
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">
Expand Down
2 changes: 1 addition & 1 deletion src/components/workspace-interior.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function WorkspaceInterior({
<WorkspaceThemeProvider>
<SidebarProvider>
<WorkspaceSidebar builder={builder} sections={sections} />
<SidebarInset>
<SidebarInset className="min-w-0">
<header className="flex h-16 shrink-0 items-center gap-2 border-b">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
Expand Down
Loading