test(import): reproduce and harden resume imports (#3471)

This commit is contained in:
Amruth Pillai
2026-09-05 19:57:01 -07:00
committed by GitHub
parent 744eaa902e
commit a6057abd79
7 changed files with 750 additions and 9 deletions
+297
View File
@@ -0,0 +1,297 @@
import type { Browser, Page, Request, Response, TestInfo } from "@playwright/test";
import type { E2EAccount } from "./data";
import { readFile } from "node:fs/promises";
import { Pool } from "pg";
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
export type ImportFormat = "pdf" | "reactive-resume-json" | "reactive-resume-v4-json" | "json-resume-json";
export type SyntheticImportFile = {
name: string;
mimeType: string;
buffer: Buffer;
declaredFormat: ImportFormat;
expectedName?: string;
};
type ImportRpcSummary = {
method: string;
path: string;
status: number;
durationMs: number;
};
const IMPORT_RPC_PATH = "/api/rpc/resume/import";
function jsonFile(
name: string,
data: unknown,
declaredFormat: Exclude<ImportFormat, "pdf">,
expectedName?: string,
): SyntheticImportFile {
return {
name,
mimeType: "application/json",
buffer: Buffer.from(JSON.stringify(data)),
declaredFormat,
expectedName,
};
}
export function currentJsonFixture(): SyntheticImportFile {
const data = structuredClone(sampleResumeData);
data.basics.name = "Current JSON Import Probe";
data.picture.hidden = true;
data.picture.url = "";
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
return jsonFile("current-resume.json", data, "reactive-resume-json", data.basics.name);
}
// Minimal synthetic shape copied from packages/import/src/reactive-resume-v4-json.test.ts.
export function v4JsonFixture(): SyntheticImportFile {
const section = (id: string) => ({
name: id,
columns: 1,
separateLinks: false,
visible: false,
id,
items: [],
});
const data = {
basics: {
name: "V4 JSON Import Probe",
headline: "Diagnostic Engineer",
email: "v4@example.test",
phone: "",
location: "Berlin",
url: { label: "", href: "" },
customFields: [],
picture: {
url: "",
size: 80,
aspectRatio: 1,
borderRadius: 0,
effects: { hidden: true, border: false, grayscale: false },
},
},
sections: {
summary: {
name: "Summary",
columns: 1,
separateLinks: false,
visible: false,
id: "summary",
content: "",
},
awards: section("awards"),
certifications: section("certifications"),
education: section("education"),
experience: {
...section("experience"),
visible: true,
items: [
{
id: "v4-experience",
visible: true,
company: "Synthetic Systems",
position: "Engineer",
location: "Berlin",
date: "2021 - Present",
summary: "Built deterministic import fixtures.",
url: { label: "", href: "" },
},
],
},
volunteer: section("volunteer"),
interests: section("interests"),
languages: section("languages"),
profiles: section("profiles"),
projects: section("projects"),
publications: section("publications"),
references: section("references"),
skills: section("skills"),
},
metadata: {
template: "onyx",
layout: [[["experience"], []]],
css: { value: "", visible: false },
page: { margin: 14, format: "a4", options: { breakLine: false, pageNumbers: false } },
theme: { background: "#ffffff", text: "#000000", primary: "#dc2626" },
typography: {
font: { family: "Helvetica", subset: "latin", variants: ["regular"], size: 10 },
lineHeight: 1.5,
hideIcons: false,
underlineLinks: false,
},
notes: "",
},
};
return jsonFile("v4-resume.json", data, "reactive-resume-v4-json", data.basics.name);
}
// Minimal synthetic shape copied from packages/import/src/json-resume.test.ts.
export function jsonResumeFixture(): SyntheticImportFile {
const data = {
basics: {
name: "JSON Resume Import Probe",
label: "Diagnostic Engineer",
email: "json-resume@example.test",
location: { city: "Berlin", countryCode: "DE" },
},
work: [
{
name: "Synthetic Systems",
position: "Engineer",
startDate: "2021-01",
endDate: "2025-01",
highlights: ["Built deterministic import fixtures"],
},
],
};
return jsonFile("json-resume.json", data, "json-resume-json", data.basics.name);
}
export function malformedJsonFixture(): SyntheticImportFile {
return {
name: "malformed.json",
mimeType: "application/json",
buffer: Buffer.from('{"basics":'),
declaredFormat: "reactive-resume-json",
};
}
export function structurallyInvalidCurrentJsonFixture(): SyntheticImportFile {
const data = structuredClone(sampleResumeData) as unknown as Record<string, unknown>;
delete data.picture;
return jsonFile("missing-picture.json", data, "reactive-resume-json");
}
export function withoutMimeType(file: SyntheticImportFile): SyntheticImportFile {
return { ...file, mimeType: "" };
}
export async function pdfFixture(
browser: Browser,
testInfo: TestInfo,
options: { blank?: boolean; mimeType?: string } = {},
): Promise<SyntheticImportFile> {
const name = options.blank ? "blank-resume.pdf" : "text-resume.pdf";
const path = testInfo.outputPath(name);
const page = await browser.newPage();
try {
await page.setContent(`<!doctype html>
<html>
<head>
<style>
@page { size: A4; margin: 18mm; }
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; line-height: 1.45; }
h1 { font-size: 22px; margin: 0 0 8px; }
h2 { font-size: 15px; margin: 20px 0 8px; text-transform: uppercase; }
</style>
</head>
<body>
${
options.blank
? ""
: `<h1>PDF Import Probe</h1>
<p>pdf-import@example.test · Berlin</p>
<h2>Experience</h2>
<p><strong>Synthetic Systems · Diagnostic Engineer</strong></p>
<p>2021 – Present</p>
<ul><li>Built deterministic browser PDF fixtures.</li><li>Verified offline resume import.</li></ul>`
}
</body>
</html>`);
await page.pdf({ path, format: "A4", printBackground: true });
} finally {
await page.close();
}
return {
name,
mimeType: options.mimeType ?? "application/pdf",
buffer: await readFile(path),
declaredFormat: "pdf",
expectedName: options.blank ? undefined : "PDF Import Probe",
};
}
export async function countUserResumes(account: E2EAccount): Promise<number> {
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL is required for import reproduction E2E.");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
try {
const result = await pool.query<{ count: string }>(
`select count(*)::text as count
from "resume"
join "user" on "user".id = "resume".user_id
where "user".email = $1`,
[account.email],
);
return Number(result.rows[0]?.count ?? 0);
} finally {
await pool.end();
}
}
export function observeImport(page: Page) {
const pageErrors: Array<{ message: string; stack: string }> = [];
const rpc: ImportRpcSummary[] = [];
const requestStart = new WeakMap<Request, number>();
const onPageError = (error: Error) => {
pageErrors.push({ message: error.message, stack: error.stack ?? error.message });
};
const onRequest = (request: Request) => {
if (new URL(request.url()).pathname !== IMPORT_RPC_PATH) return;
requestStart.set(request, performance.now());
};
const onResponse = (response: Response) => {
const request = response.request();
const path = new URL(response.url()).pathname;
if (path !== IMPORT_RPC_PATH) return;
rpc.push({
method: request.method(),
path,
status: response.status(),
durationMs: Math.round(performance.now() - (requestStart.get(request) ?? performance.now())),
});
};
page.on("pageerror", onPageError);
page.on("request", onRequest);
page.on("response", onResponse);
return {
pageErrors,
rpc,
async attach(
testInfo: TestInfo,
details: {
fixture: string;
detectedFormat: ImportFormat | "";
selectedFormat: ImportFormat;
providerState: "none";
uiOutcome: string;
beforeCount: number;
afterCount: number;
},
) {
await testInfo.attach("import-diagnostics", {
contentType: "application/json",
body: Buffer.from(JSON.stringify({ ...details, rpc, pageErrors }, null, 2)),
});
},
dispose() {
page.off("pageerror", onPageError);
page.off("request", onRequest);
page.off("response", onResponse);
},
};
}
+357
View File
@@ -0,0 +1,357 @@
import type { Locator, Page, TestInfo } from "@playwright/test";
import type { E2EAccount } from "../fixtures/data";
import type { ImportFormat, SyntheticImportFile } from "../fixtures/import";
import { parseJSONResume } from "@reactive-resume/import/json-resume";
import { parseReactiveResumeJSON } from "@reactive-resume/import/reactive-resume-json";
import { parseReactiveResumeV4JSON } from "@reactive-resume/import/reactive-resume-v4-json";
import {
countUserResumes,
currentJsonFixture,
jsonResumeFixture,
malformedJsonFixture,
observeImport,
pdfFixture,
structurallyInvalidCurrentJsonFixture,
v4JsonFixture,
withoutMimeType,
} from "../fixtures/import";
import { expect, test } from "../fixtures/test";
const formatLabels: Record<ImportFormat, string> = {
"reactive-resume-json": "Reactive Resume (JSON)",
"reactive-resume-v4-json": "Reactive Resume v4 (JSON)",
"json-resume-json": "JSON Resume",
pdf: "PDF",
};
async function openImportDialog(page: Page): Promise<Locator> {
await page.goto("/dashboard/resumes");
await page.getByText("Import an existing resume", { exact: true }).click();
return page.getByRole("dialog", { name: "Import an existing resume" });
}
async function selectImportFile(
page: Page,
dialog: Locator,
file: SyntheticImportFile,
selectedFormat = file.declaredFormat,
): Promise<void> {
await dialog.locator('input[type="file"]').setInputFiles({
name: file.name,
mimeType: file.mimeType,
buffer: file.buffer,
});
const combobox = dialog.getByRole("combobox");
await expect(combobox).toContainText(formatLabels[file.declaredFormat]);
if (selectedFormat === file.declaredFormat) return;
await combobox.click();
await page.getByRole("option", { name: formatLabels[selectedFormat], exact: true }).click();
await expect(combobox).toContainText(formatLabels[selectedFormat]);
}
async function assertImportedName(page: Page, expectedName: string): Promise<void> {
await page.waitForURL(/\/builder\/.+/);
await page.getByTestId("left").getByRole("button", { name: "Basics", exact: true }).click();
await expect(page.getByLabel("Name", { exact: true })).toHaveValue(expectedName);
}
async function assertErrorToast(page: Page, expected: string | RegExp, timeout = 10_000): Promise<string> {
const toast = page.locator('[data-slot="toast"][data-type="error"]');
await expect(toast).toBeVisible({ timeout });
const description = toast.locator('[data-slot="toast-description"]');
await expect(description).toContainText(expected);
return (await description.textContent()) ?? "";
}
async function attachDiagnostics(
observation: ReturnType<typeof observeImport>,
testInfo: TestInfo,
details: {
fixture: string;
detectedFormat: ImportFormat | "";
selectedFormat: ImportFormat;
uiOutcome: string;
beforeCount: number;
afterCount: number;
},
): Promise<void> {
await observation.attach(testInfo, { ...details, providerState: "none" });
observation.dispose();
}
async function importSuccessfully(
page: Page,
account: E2EAccount,
testInfo: TestInfo,
file: SyntheticImportFile,
): Promise<void> {
const observation = observeImport(page);
const beforeCount = await countUserResumes(account);
let afterCount = beforeCount;
let uiOutcome = "test failed before UI outcome";
try {
const dialog = await openImportDialog(page);
await selectImportFile(page, dialog, file);
await dialog.getByRole("button", { name: "Import", exact: true }).click();
await assertImportedName(page, file.expectedName ?? "");
await expect.poll(() => countUserResumes(account)).toBe(beforeCount + 1);
afterCount = await countUserResumes(account);
uiOutcome = `builder:${new URL(page.url()).pathname}`;
expect(observation.rpc).toEqual([
expect.objectContaining({ method: "POST", path: "/api/rpc/resume/import", status: 200 }),
]);
expect(observation.pageErrors).toEqual([]);
} finally {
await attachDiagnostics(observation, testInfo, {
fixture: file.name,
detectedFormat: file.declaredFormat,
selectedFormat: file.declaredFormat,
uiOutcome,
beforeCount,
afterCount,
});
}
}
test.describe("synthetic import fixtures", () => {
test("valid JSON fixtures parse through their declared importers", () => {
const current = currentJsonFixture();
const v4 = v4JsonFixture();
const jsonResume = jsonResumeFixture();
expect(parseReactiveResumeJSON(current.buffer.toString()).basics.name).toBe(current.expectedName);
expect(parseReactiveResumeV4JSON(v4.buffer.toString()).basics.name).toBe(v4.expectedName);
expect(parseJSONResume(jsonResume.buffer.toString()).basics.name).toBe(jsonResume.expectedName);
});
test("invalid JSON fixtures fail for their intended reason", () => {
expect(() => parseReactiveResumeJSON(malformedJsonFixture().buffer.toString())).toThrow(/JSON/i);
expect(() => parseReactiveResumeJSON(structurallyInvalidCurrentJsonFixture().buffer.toString())).toThrow(
/picture/i,
);
});
});
for (const fixtureFactory of [v4JsonFixture, jsonResumeFixture]) {
const fixture = fixtureFactory();
test(`imports ${fixture.declaredFormat} through real dialog and RPC`, async ({
authPage: page,
account,
}, testInfo) => {
await importSuccessfully(page, account, testInfo, fixtureFactory());
});
}
test("imports current JSON while close cancellation preserves pending dialog state", async ({
authPage: page,
account,
}, testInfo) => {
const file = currentJsonFixture();
const observation = observeImport(page);
const beforeCount = await countUserResumes(account);
let afterCount = beforeCount;
let uiOutcome = "test failed before UI outcome";
let releaseImport = () => {};
const barrier = new Promise<void>((resolve) => {
releaseImport = resolve;
});
let interceptedImport = () => {};
const intercepted = new Promise<void>((resolve) => {
interceptedImport = resolve;
});
await page.route("**/api/rpc/resume/import", async (route) => {
interceptedImport();
await barrier;
try {
await route.continue();
} catch (error) {
if (!page.isClosed()) throw error;
}
});
try {
const dialog = await openImportDialog(page);
await selectImportFile(page, dialog, file);
await dialog.getByRole("button", { name: "Import", exact: true }).click();
await intercepted;
await dialog.getByRole("button", { name: "Close", exact: true }).click();
const confirmation = page.getByRole("alertdialog", { name: "Are you sure you want to close this dialog?" });
await expect(confirmation).toBeVisible();
await confirmation.getByRole("button", { name: "Stay", exact: true }).click();
await expect(dialog).toBeVisible();
await expect(dialog.getByText(file.name, { exact: true })).toBeVisible();
await expect(dialog.getByRole("button", { name: /Importing/ })).toBeDisabled();
releaseImport();
await assertImportedName(page, file.expectedName ?? "");
await expect.poll(() => countUserResumes(account)).toBe(beforeCount + 1);
afterCount = await countUserResumes(account);
uiOutcome = `close-cancelled:file-retained;builder:${new URL(page.url()).pathname}`;
expect(observation.rpc).toEqual([
expect.objectContaining({ method: "POST", path: "/api/rpc/resume/import", status: 200 }),
]);
expect(observation.pageErrors).toEqual([]);
} finally {
releaseImport();
if (!page.isClosed()) await page.unroute("**/api/rpc/resume/import");
await attachDiagnostics(observation, testInfo, {
fixture: file.name,
detectedFormat: file.declaredFormat,
selectedFormat: file.declaredFormat,
uiOutcome,
beforeCount,
afterCount,
});
}
});
test("imports current JSON when browser supplies no MIME metadata", async ({ authPage: page, account }, testInfo) => {
await importSuccessfully(page, account, testInfo, withoutMimeType(currentJsonFixture()));
});
test("imports offline text PDF generated by Playwright Chromium", async ({
authPage: page,
account,
browser,
}, testInfo) => {
test.setTimeout(60_000);
await importSuccessfully(page, account, testInfo, await pdfFixture(browser, testInfo));
});
test("imports magic-byte PDF when browser supplies no MIME metadata", async ({
authPage: page,
account,
browser,
}, testInfo) => {
test.setTimeout(60_000);
await importSuccessfully(page, account, testInfo, await pdfFixture(browser, testInfo, { mimeType: "" }));
});
test("keeps blank PDF in dialog and creates no resume", async ({ authPage: page, account, browser }, testInfo) => {
test.setTimeout(60_000);
const file = await pdfFixture(browser, testInfo, { blank: true });
const observation = observeImport(page);
const beforeCount = await countUserResumes(account);
let afterCount = beforeCount;
let uiOutcome = "test failed before UI outcome";
try {
const dialog = await openImportDialog(page);
await selectImportFile(page, dialog, file);
await dialog.getByRole("button", { name: "Import", exact: true }).click();
await assertErrorToast(
page,
"This PDF has no readable text. It is likely a scan, so there is nothing to import.",
30_000,
);
await expect(dialog).toBeVisible();
await expect(dialog.getByText(file.name, { exact: true })).toBeVisible();
afterCount = await countUserResumes(account);
uiOutcome = "error:no-readable-text;dialog-and-file-retained";
expect(afterCount).toBe(beforeCount);
expect(observation.rpc).toEqual([]);
expect(observation.pageErrors).toEqual([]);
} finally {
await attachDiagnostics(observation, testInfo, {
fixture: file.name,
detectedFormat: file.declaredFormat,
selectedFormat: file.declaredFormat,
uiOutcome,
beforeCount,
afterCount,
});
}
});
for (const file of [malformedJsonFixture(), structurallyInvalidCurrentJsonFixture()]) {
test(`keeps invalid fixture ${file.name} in dialog and creates no resume`, async ({
authPage: page,
account,
}, testInfo) => {
const observation = observeImport(page);
const beforeCount = await countUserResumes(account);
let afterCount = beforeCount;
let uiOutcome = "test failed before UI outcome";
try {
const dialog = await openImportDialog(page);
await dialog.locator('input[type="file"]').setInputFiles({
name: file.name,
mimeType: file.mimeType,
buffer: file.buffer,
});
const detectedFormat = file.name === "malformed.json" ? "" : file.declaredFormat;
if (detectedFormat === "") {
await expect(dialog.getByText("We couldn't detect the format automatically. Choose it above.")).toBeVisible();
await dialog.getByRole("combobox").click();
await page.getByRole("option", { name: formatLabels[file.declaredFormat], exact: true }).click();
} else {
await expect(dialog.getByRole("combobox")).toContainText(formatLabels[file.declaredFormat]);
}
await dialog.getByRole("button", { name: "Import", exact: true }).click();
const expectedError =
file.name === "malformed.json"
? /Unexpected end of JSON input/
: /The file could not be read as a valid resume/;
await assertErrorToast(page, expectedError);
await expect(dialog.getByText(file.name, { exact: true })).toBeVisible();
afterCount = await countUserResumes(account);
uiOutcome = "parse-error;dialog-and-file-retained";
expect(afterCount).toBe(beforeCount);
expect(observation.rpc).toEqual([]);
expect(observation.pageErrors).toEqual([]);
} finally {
await attachDiagnostics(observation, testInfo, {
fixture: file.name,
detectedFormat: file.name === "malformed.json" ? "" : file.declaredFormat,
selectedFormat: file.declaredFormat,
uiOutcome,
beforeCount,
afterCount,
});
}
});
}
test("rejects a JSON Resume deliberately selected as current JSON without creating a resume", async ({
authPage: page,
account,
}, testInfo) => {
const file = jsonResumeFixture();
const selectedFormat = "reactive-resume-json" as const;
const observation = observeImport(page);
const beforeCount = await countUserResumes(account);
let afterCount = beforeCount;
let uiOutcome = "test failed before UI outcome";
try {
const dialog = await openImportDialog(page);
await selectImportFile(page, dialog, file, selectedFormat);
await dialog.getByRole("button", { name: "Import", exact: true }).click();
await assertErrorToast(page, /The file could not be read as a valid resume/);
await expect(dialog.getByText(file.name, { exact: true })).toBeVisible();
afterCount = await countUserResumes(account);
uiOutcome = "selected-format-parse-error;dialog-and-file-retained";
expect(afterCount).toBe(beforeCount);
expect(observation.rpc).toEqual([]);
expect(observation.pageErrors).toEqual([]);
} finally {
await attachDiagnostics(observation, testInfo, {
fixture: file.name,
detectedFormat: file.declaredFormat,
selectedFormat,
uiOutcome,
beforeCount,
afterCount,
});
}
});