mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-09-29 16:24:22 +10:00
test(import): reproduce and harden resume imports (#3471)
This commit is contained in:
@@ -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);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user