feat(cv): rebuild section editing workflow
CI and Deploy / test (push) Failing after 30s
CI and Deploy / deploy (push) Has been skipped

This commit is contained in:
cesnimda
2026-08-27 15:27:54 +02:00
parent ccd0af908c
commit b5249357c7
10 changed files with 525 additions and 221 deletions
+145 -2
View File
@@ -315,6 +315,26 @@ test("Career Workspace loads from the authenticated application shell", async ({
await expect(page).toHaveURL(/\/career\/builder\/\d+$/);
await expect(page.getByLabel("CV name")).toBeVisible();
await page.getByRole("button", { name: "Expand Professional Summary" }).click();
await page.getByRole("button", { name: "Expand Experience" }).click();
await expect(page.getByRole("button", { name: "Collapse Professional Summary" })).toHaveAttribute("aria-expanded", "true");
await expect(page.getByRole("button", { name: "Collapse Experience" })).toHaveAttribute("aria-expanded", "true");
await page.getByLabel("Section type").click();
await page.getByRole("option", { name: "Additional Experience" }).click();
await page.getByRole("button", { name: "Add section" }).click();
await page.getByRole("button", { name: "Expand Additional Experience" }).click();
await page.getByRole("button", { name: "Add entry" }).click();
await page.getByRole("textbox", { name: "Entry 1", exact: true }).fill("Maintained a community software project.");
await page.getByRole("tab", { name: "Template" }).click();
await page.getByRole("button").filter({ hasText: /^CodeTechnical/ }).click();
await page.getByRole("tab", { name: "Design" }).click();
await page.getByLabel("Skills presentation").click();
await page.getByRole("option", { name: "Bullet list" }).click();
await page.locator('input[type="color"]').fill("#126b55");
await expect(page.getByRole("tab", { name: "AI" })).toHaveCount(0);
await page.evaluate(() => window.localStorage.setItem("jobtracker.themeMode", "dark"));
await page.reload();
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark");
@@ -331,7 +351,7 @@ test("Career Workspace loads from the authenticated application shell", async ({
}
});
test("a public CV renders anonymously and downloads as PDF", async ({ page }) => {
test("a public CV renders anonymously and downloads as PDF", async ({ page }, testInfo) => {
await login(page);
const headers = await csrfHeader(page);
const create = await page.request.post(`${apiUrl}/cv/variants`, {
@@ -368,5 +388,128 @@ test("a public CV renders anonymously and downloads as PDF", async ({ page }) =>
const pdf = await page.request.get(`${apiUrl}/public-cv/${published.publicSlug}/pdf`);
expect(pdf.ok()).toBeTruthy();
expect(pdf.headers()["content-type"]).toContain("application/pdf");
expect((await pdf.body()).subarray(0, 5).toString()).toBe("%PDF-");
const pdfBody = await pdf.body();
expect(pdfBody.length).toBeGreaterThan(1_000);
expect(pdfBody.subarray(0, 5).toString()).toBe("%PDF-");
await testInfo.attach("public-cv.pdf", { body: pdfBody, contentType: "application/pdf" });
});
test("the Code template exports a long structured CV as searchable multi-page PDF", async ({ page }, testInfo) => {
await login(page);
const headers = await csrfHeader(page);
const longBullets = [
"Designed resilient .NET services that processed high-volume workloads while preserving clear operational ownership and measurable service health.",
"Improved deployment safety through automated checks, progressive delivery, actionable telemetry, and documented incident-response procedures.",
"Collaborated with product, security, and support teams across Norway and the United Kingdom to turn ambiguous requirements into maintainable releases.",
];
const profile = {
version: "1",
contact: {
fullName: "Alex Eksempel",
headline: "Principal Software Engineer",
email: "alex.eksempel+career-verification@example.test",
phone: "+47 412 34 567",
location: "Oslo, Norge",
website: "https://example.test/portfolio/with/a/deliberately/long/path",
linkedIn: "https://linkedin.com/in/alex-eksempel",
gitHub: "https://github.com/alex-eksempel",
},
summary: ["Teknisk leder med erfaring fra robuste plattformer, tilgjengelige tjenester og tverrfaglig produktutvikling."],
jobs: Array.from({ length: 9 }, (_, index) => ({
title: `Senior Platform Engineer ${index + 1} With A Deliberately Long Role Name`,
company: `Example Technology Organisation ${index + 1} With A Long Employer Name`,
location: index % 2 === 0 ? "Oslo, Norge" : "Warwickshire, United Kingdom",
start: `${2015 + index}`,
end: index === 8 ? "Present" : `${2016 + index}`,
isCurrent: index === 8,
bullets: longBullets,
skills: ["C#", ".NET", "PostgreSQL", "Docker", "Azure DevOps"],
})),
education: [{ qualification: "BSc Software Engineering", institution: "Example University", start: "2011", end: "2015", details: ["Distributed systems and human-computer interaction"] }],
certifications: [{ name: "Cloud Architecture Professional", issuer: "Example Institute", date: "2025" }],
projects: [{ name: "Open Source Reliability Toolkit", role: "Maintainer", start: "2022", end: "Present", bullets: longBullets.slice(0, 2), skills: ["TypeScript", "Playwright"] }],
skills: ["C# / .NET", "Python", "SQL", "Docker", "Azure DevOps", "Linux", "Observability", "Incident response"],
languages: [{ name: "English", level: "Native" }, { name: "Norsk", level: "Profesjonelt arbeidsnivå" }],
interests: ["Tilgjengelighet", "Open source"],
otherSections: [{ title: "Additional Experience", items: ["Mentored early-career engineers", "Organised community technology workshops"] }],
};
const profileResponse = await page.request.put(`${apiUrl}/career/profile`, {
headers,
data: { profile, cvText: "Long sanitized CV fixture for PDF verification." },
});
expect(profileResponse.ok()).toBeTruthy();
const create = await page.request.post(`${apiUrl}/cv/variants`, {
headers,
data: {
name: "Code PDF Verification",
settings: {
themeId: "code",
accent: "#0b7a63",
pageSize: "a4",
skillsStyle: "bullets",
language: "nb",
},
},
});
const createBody = await create.text();
expect(create.ok(), `CV create failed: ${create.status()} ${createBody}`).toBeTruthy();
const variant = JSON.parse(createBody);
const preview = await page.request.get(`${apiUrl}/cv/variants/${variant.id}/preview`);
expect(preview.ok()).toBeTruthy();
const previewBody = await preview.json();
expect(previewBody.themeId).toBe("code");
expect(previewBody.html).toContain("Alex Eksempel");
expect(previewBody.html).toContain("--cv-accent-color:#0b7a63");
await page.goto(`/career/builder/${variant.id}`);
await expect(page.getByLabel("CV name")).toHaveValue("Code PDF Verification");
await expect(page.getByTitle("CV preview page 2")).toHaveCount(1, { timeout: 10_000 });
const pagination = await page.getByTitle("CV preview", { exact: true }).evaluate((element) => {
const frame = element as HTMLIFrameElement;
const doc = frame.contentDocument;
const height = 297 * 96 / 25.4;
if (!doc || height <= 0) return { scriptRan: false, crossings: ["document unavailable"] };
const topOf = (node: Element) => {
let top = 0;
let current = node as HTMLElement | null;
while (current && current !== doc.body) {
top += current.offsetTop || 0;
current = current.offsetParent as HTMLElement | null;
}
return top;
};
const candidates = Array.from(doc.querySelectorAll<HTMLElement>(
".section:not(.section-flow),.section-flow > .entry:not(.entry-flow),.section-flow > .skill-groups > .skill-group,.section-flow > .bullets > li:not(.item-flow),.entry-flow .bullets > li:not(.item-flow)",
));
const crossings = candidates.filter((candidate) => {
const paddingTop = Number.parseFloat(getComputedStyle(candidate).paddingTop) || 0;
const top = topOf(candidate) + paddingTop;
const contentHeight = candidate.offsetHeight - paddingTop;
const bottom = top + contentHeight;
return contentHeight < height - 2
&& Math.floor((top + 1) / height) !== Math.floor((bottom - 1) / height);
}).map((candidate) => ({
className: candidate.className || candidate.tagName,
top: topOf(candidate) + (Number.parseFloat(getComputedStyle(candidate).paddingTop) || 0),
height: candidate.offsetHeight,
paddingTop: getComputedStyle(candidate).paddingTop,
}));
return {
scriptRan: !!doc.querySelector("script[data-cv-preview-pagination]"),
crossings,
};
});
expect(pagination.scriptRan).toBeTruthy();
expect(pagination.crossings).toEqual([]);
const pdf = await page.request.post(`${apiUrl}/cv/variants/${variant.id}/export-pdf`, { headers });
expect(pdf.ok()).toBeTruthy();
expect(pdf.headers()["content-type"]).toContain("application/pdf");
const pdfBody = await pdf.body();
expect(pdfBody.length).toBeGreaterThan(20_000);
expect(pdfBody.subarray(0, 5).toString()).toBe("%PDF-");
await testInfo.attach("code-long-cv.pdf", { body: pdfBody, contentType: "application/pdf" });
});