fix(workspace): clear native package and navigation readiness gates

This commit is contained in:
cesnimda
2026-10-07 00:30:10 +02:00
parent fadf99a3f5
commit acd1d94684
36 changed files with 1551 additions and 40 deletions
@@ -1,4 +1,7 @@
import { expect, test as base, type APIRequestContext } from "@playwright/test";
import { createHash } from "node:crypto";
import { buildJobWorkspacePath } from "../src/jobWorkspaceRoute";
import AxeBuilder from "@axe-core/playwright";
const apiUrl = "http://localhost:5302/api";
const password = "E2ePassword123!";
@@ -67,7 +70,8 @@ test("[PRES-C01 PRES-C06 PRES-C07 PRES-C10] mobile CV override persists without
expect((await (await page.request.get(`${apiUrl}/career/profile`)).json()).profile).toEqual(profileBefore.profile);
});
test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", async ({ page, headers }) => {
test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves frozen documents after current sources change", async ({ page, headers, playwright }, testInfo) => {
test.setTimeout(90_000);
const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Preservation AS" } });
expect(companyResponse.status()).toBe(201);
const company = await companyResponse.json();
@@ -77,9 +81,14 @@ test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves
const variantResponse = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Submitted CV", jobApplicationId: job.id } });
expect(variantResponse.status()).toBe(200);
const variant = await variantResponse.json();
const siblingResponse = await page.request.post(`${apiUrl}/cv/variants`, { headers, data: { name: "Sibling CV" } });
expect(siblingResponse.status()).toBe(200);
const sibling = await siblingResponse.json();
const jobUrl = `${apiUrl}/jobapplications/${job.id}`;
expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: variant.id } })).status()).toBe(200);
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Original reviewed letter", source: "manual" } })).status()).toBe(200);
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { applicationAnswerDraft: "Original answer — æøå", recruiterMessageDraft: "Original recruiter message" } })).status()).toBe(204);
expect((await page.request.get(`${jobUrl}/tailoring`)).status()).toBe(200);
const bytes = Buffer.from("Original attachment — æøå\n", "utf8");
const upload = await page.request.post(`${apiUrl}/attachments`, { headers, multipart: { jobId: String(job.id), files: { name: "evidence.txt", mimeType: "text/plain", buffer: bytes } } });
expect(upload.ok()).toBe(true);
@@ -89,12 +98,8 @@ test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves
const captured = page.waitForResponse(response => response.url() === `${jobUrl}/submitted-packages` && response.request().method() === 'POST');
await page.getByRole("button", { name: "Freeze current package", exact: true }).click();
const captureResponse = await captured;
// P2-D01: native SQLite rejects the service's DateTimeOffset ORDER BY. Only
// this known response is expected; setup failures and later archive failures
// after a successful capture must remain ordinary failing assertions.
if (captureResponse.status() === 500) {
test.fail(true, 'P2-D01: SQLite submitted-package capture; phase-2/BASELINE_DEFECTS.md');
}
// P2-D01 is now an ordinary regression: native capture AND preserved readback
// must succeed; a capture 500 can no longer be classified as expected.
expect(captureResponse.status()).toBe(200);
await expect(page.getByText("Submission package v1", { exact: true })).toBeVisible();
const [snapshot] = await (await page.request.get(`${jobUrl}/submitted-packages`)).json();
@@ -102,19 +107,105 @@ test("[PRES-W01 PRES-W05 PRES-W06 PRES-W07 PRES-W08] legacy workspace preserves
expect(before.coverLetterText).toBe("Original reviewed letter");
expect(before.renderedCvHtml).toContain("Canonical engineer");
expect(snapshot.cvVariantId).toBe(variant.id);
expect(snapshot.cvVariantVersion).toBe(variant.version);
expect(before.applicationMaterialJson).toContain("Original answer");
expect(snapshot.attachments).toHaveLength(1);
expect((await page.request.put(`${jobUrl}/cover-letter`, { headers, data: { text: "Later letter", source: "manual" } })).status()).toBe(200);
expect((await page.request.put(`${apiUrl}/cv/variants/${variant.id}`, { headers, data: { name: "Later CV", settings: { ...variant.settings, headline: "Later headline" } } })).status()).toBe(200);
expect((await page.request.put(`${apiUrl}/career/profile`, { headers, data: { profile: { ...canonical, contact: { ...canonical.contact, headline: "Later canonical headline" } } } })).status()).toBe(200);
expect((await page.request.put(`${jobUrl}/application-drafts`, { headers, data: { applicationAnswerDraft: "Later answer", recruiterMessageDraft: "Later recruiter message" } })).status()).toBe(204);
expect((await page.request.delete(`${apiUrl}/attachments/${attachment.id}`, { headers })).status()).toBe(204);
expect(await (await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}`)).json()).toEqual(before);
const archived = await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}/attachments/${snapshot.attachments[0].id}`);
expect(archived.status()).toBe(200);
expect(await archived.body()).toEqual(bytes);
expect((await page.request.put(`${jobUrl}/cv`, { headers, data: { variantId: null } })).status()).toBe(200);
expect((await page.request.get(`${apiUrl}/cv/variants/${variant.id}`)).status()).toBe(200);
expect(await (await page.request.get(`${apiUrl}/cv/variants/${sibling.id}`)).json()).toEqual(sibling);
const restored = await page.request.post(`${jobUrl}/cover-letter/versions/1/restore`, { headers });
expect(restored.status()).toBe(200);
expect((await restored.json()).currentVersion).toBe(3);
expect((await (await page.request.get(`${jobUrl}/cover-letter`)).json()).text).toBe("Original reviewed letter");
expect(await (await page.request.get(`${jobUrl}/submitted-packages/${snapshot.id}`)).json()).toEqual(before);
await page.reload();
await page.getByRole("button", { name: "View preserved package", exact: true }).click();
await expect(page.getByRole("dialog").getByText("Original reviewed letter", { exact: true })).toBeVisible();
await testInfo.attach("native-frozen-package", { body: await page.screenshot({ fullPage: true }), contentType: "image/png" });
await testInfo.attach("native-preservation-invariants", { body: JSON.stringify({ packageId: snapshot.id, version: snapshot.version, cvVariantId: snapshot.cvVariantId, cvVariantVersion: snapshot.cvVariantVersion, attachment: { name: snapshot.attachments[0].fileName, sha256: createHash("sha256").update(bytes).digest("hex"), bytes: bytes.length }, immutableReadback: true, canonicalAndVariantAndLetterAndAnswerMutated: true, liveAttachmentDeleted: true, siblingUnchanged: true, detachPreservedVariant: true }), contentType: "application/json" });
const admin = await playwright.request.newContext();
const other = await playwright.request.newContext();
try {
const adminHeaders = await login(admin, "e2e@example.test");
const email = `package-other-${Date.now()}@example.test`;
expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200);
const otherHeaders = await login(other, email);
for (const path of [`${jobUrl}/submitted-packages`, `${jobUrl}/submitted-packages/${snapshot.id}`, `${jobUrl}/submitted-packages/${snapshot.id}/attachments/${snapshot.attachments[0].id}`]) expect((await other.get(path)).status()).toBe(404);
expect((await other.put(`${jobUrl}/cv`, { headers: otherHeaders, data: { variantId: variant.id } })).status()).toBe(404);
expect((await other.post(`${jobUrl}/submitted-packages`, { headers: otherHeaders })).status()).toBe(404);
const otherCompany = await other.post(`${apiUrl}/companies`, { headers: otherHeaders, data: { name: "Other-owner AS" } });
expect(otherCompany.status()).toBe(201);
const otherJob = await other.post(`${apiUrl}/jobapplications`, { headers: otherHeaders, data: { companyId: (await otherCompany.json()).id, jobTitle: "Other owner", status: "Saved" } });
expect(otherJob.status()).toBe(201);
expect((await other.put(`${apiUrl}/jobapplications/${(await otherJob.json()).id}/cv`, { headers: otherHeaders, data: { variantId: variant.id } })).status()).toBe(404);
} finally { await other.dispose(); await admin.dispose(); }
});
for (const language of ["en", "nb"] as const) {
for (const theme of ["light", "dark"] as const) {
test(`[PRES-W01 PRES-W04 PRES-W08 PRES-E08 PRES-E09] native workspace aliases, keyboard sections and history retain context (${language}, ${theme})`, async ({ page, headers }, testInfo) => {
test.setTimeout(120_000);
await page.addInitScript(({ locale, mode }) => { localStorage.setItem("uiLanguage", locale); localStorage.setItem("jobtracker.themeMode", mode); }, { locale: language, mode: theme });
expect((await page.request.put(`${apiUrl}/auth/preferences/language`, { headers, data: { language: language === "nb" ? "nb-NO" : "en" } })).status()).toBe(204);
const companyResponse = await page.request.post(`${apiUrl}/companies`, { headers, data: { name: "Navigasjon AS" } });
expect(companyResponse.status()).toBe(201);
const created = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: { companyId: (await companyResponse.json()).id, jobTitle: "Readiness workspace", status: "Saved", description: "C# engineer" } });
expect(created.ok()).toBe(true);
const job = await created.json();
const expectedSections = ["overview", "communication", "documents", "cv", "communication", "match", "analysis", "interview", "checklist", "timeline"];
for (let tab = 0; tab < expectedSections.length; tab++) {
const path = buildJobWorkspacePath(job.id, { tab, followMode: "waiting-update" });
await page.goto(path);
await expect(page.getByRole("heading", { name: "Readiness workspace", exact: true })).toBeVisible();
expect(new URL(page.url()).searchParams.get("followMode")).toBe("waiting-update");
const section = expectedSections[tab];
if (["communication", "documents", "checklist", "timeline"].includes(section)) {
const names = language === "en" ? { communication: "Communication", documents: "Documents", checklist: "Next actions and checklist", timeline: "Activity history" } : { communication: "Kommunikasjon", documents: "Dokumenter", checklist: "Neste handlinger og sjekkliste", timeline: "Aktivitetshistorikk" };
await expect(page.getByRole("button", { name: names[section as keyof typeof names], exact: true })).toHaveAttribute("aria-expanded", "true");
}
await expect(page.getByRole("tabpanel")).toBeVisible();
}
await page.goto(`/jobs/${job.id}?section=checklist&followMode=waiting-update`);
const move = page.getByRole("button", { name: language === "en" ? /^Move up:/ : /^Flytt opp:/ }).first();
await move.focus();
await expect(move).toBeFocused();
await expect.poll(() => move.evaluate(node => getComputedStyle(node.closest(".checklist-actions")!).opacity)).toBe("1");
await testInfo.attach(`workspace-native-desktop-${language}-${theme}`, { body: await page.screenshot({ fullPage: true }), contentType: "image/png" });
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(`/jobs/${job.id}?section=documents&followMode=waiting-update`);
await page.reload();
await expect(page.getByRole("heading", { name: "Readiness workspace", exact: true })).toBeVisible();
const navigationScan = await new AxeBuilder({ page }).include('[role="tablist"]').analyze();
await testInfo.attach("navigation-axe", { body: JSON.stringify({ violations: navigationScan.violations, incomplete: navigationScan.incomplete }), contentType: "application/json" });
expect(navigationScan.violations).toEqual([]);
await testInfo.attach(`workspace-native-disclosure-${language}-${theme}`, { body: await page.screenshot({ fullPage: true }), contentType: "image/png" });
const overviewTab = page.getByRole("tab", { name: language === "en" ? "Overview" : "Oversikt", exact: true });
await overviewTab.focus();
await page.keyboard.press("ArrowRight");
const analysisTab = page.getByRole("tab", { name: language === "en" ? "Analysis" : "Analyse", exact: true });
await expect(analysisTab).toBeFocused();
await page.keyboard.press("Enter");
await expect(analysisTab).toHaveAttribute("aria-selected", "true");
expect(new URL(page.url()).searchParams.get("followMode")).toBe("waiting-update");
await page.goBack();
await expect(overviewTab).toHaveAttribute("aria-selected", "true");
expect(new URL(page.url()).searchParams.get("section")).toBe("documents");
await page.goForward();
await expect(analysisTab).toHaveAttribute("aria-selected", "true");
await testInfo.attach(`workspace-native-navigation-${language}-${theme}`, { body: await page.screenshot({ fullPage: true }), contentType: "image/png" });
});
}
}
test("[PRES-S03 PRES-W11] responsive settings navigation keeps rule edits and persists notification preferences", async ({ page, headers }) => {
expect((await page.request.get(`${apiUrl}/auth/me`, { headers })).status()).toBe(200);
await page.setViewportSize({ width: 390, height: 844 });