9edcbfc5de
Make job/CV comparisons language-aware and filter recruitment noise. Improve responsive career navigation, shared spacing, dashboard priorities, settings, localized workspace controls, and portable browser tests.
571 lines
29 KiB
TypeScript
571 lines
29 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
const apiUrl = "http://localhost:5302/api";
|
|
|
|
type Rgba = { red: number; green: number; blue: number; alpha: number };
|
|
|
|
function parseCssColour(value: string): Rgba {
|
|
const parts = value.match(/[\d.]+/g)?.map(Number) ?? [];
|
|
expect(parts.length, `expected an rgb/rgba colour but received ${value}`).toBeGreaterThanOrEqual(3);
|
|
return { red: parts[0], green: parts[1], blue: parts[2], alpha: parts[3] ?? 1 };
|
|
}
|
|
|
|
function composite(foreground: Rgba, background: Rgba): Rgba {
|
|
return {
|
|
red: foreground.red * foreground.alpha + background.red * (1 - foreground.alpha),
|
|
green: foreground.green * foreground.alpha + background.green * (1 - foreground.alpha),
|
|
blue: foreground.blue * foreground.alpha + background.blue * (1 - foreground.alpha),
|
|
alpha: 1,
|
|
};
|
|
}
|
|
|
|
function relativeLuminance(colour: Rgba) {
|
|
const linear = [colour.red, colour.green, colour.blue].map((channel) => {
|
|
const value = channel / 255;
|
|
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
});
|
|
return linear[0] * 0.2126 + linear[1] * 0.7152 + linear[2] * 0.0722;
|
|
}
|
|
|
|
function contrastRatio(foreground: Rgba, background: Rgba) {
|
|
const foregroundLuminance = relativeLuminance(foreground);
|
|
const backgroundLuminance = relativeLuminance(background);
|
|
return (Math.max(foregroundLuminance, backgroundLuminance) + 0.05)
|
|
/ (Math.min(foregroundLuminance, backgroundLuminance) + 0.05);
|
|
}
|
|
|
|
async function loginAs(page: Page, email: string, password: string) {
|
|
await page.goto("/login");
|
|
await page.getByLabel("Email").fill(email);
|
|
await page.getByLabel("Current password").fill(password);
|
|
await page.getByRole("button", { name: "Sign in", exact: true }).click();
|
|
await expect(page).toHaveURL(/\/dashboard$/);
|
|
await expect(page.getByRole("heading", { name: "Dashboard", exact: true })).toBeVisible();
|
|
}
|
|
|
|
async function login(page: Page) {
|
|
await loginAs(page, "e2e@example.test", "E2ePassword123!");
|
|
}
|
|
|
|
async function csrfHeader(page: Page) {
|
|
const cookie = (await page.context().cookies()).find((value) => value.name === "XSRF-TOKEN");
|
|
expect(cookie, "login should issue a CSRF cookie").toBeTruthy();
|
|
return { "X-CSRF-TOKEN": cookie!.value };
|
|
}
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.addInitScript(() => window.localStorage.setItem("uiLanguage", "en"));
|
|
});
|
|
|
|
test("public plans stay honest, responsive and keyboard accessible", async ({ page }) => {
|
|
for (const scheme of ["light", "dark"] as const) {
|
|
await page.addInitScript((value) => window.localStorage.setItem("jobtracker.themeMode", value), scheme);
|
|
await page.goto("/");
|
|
await expect(page.getByRole("heading", { name: "Two clear plans" })).toBeVisible();
|
|
await expect(page.locator('section[aria-labelledby^="plan-"]')).toHaveCount(2);
|
|
await expect(page.getByRole("heading", { name: "Free", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "Pro", exact: true })).toBeVisible();
|
|
await expect(page.getByText(/Bring your own key|Unlimited AI|Billed monthly|Most popular/i)).toHaveCount(0);
|
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", scheme);
|
|
|
|
for (const width of [375, 768, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
}
|
|
|
|
await page.goto("/login");
|
|
await expect(page.getByRole("heading", { name: "Sign in", exact: true })).toBeVisible();
|
|
for (const width of [375, 768, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
}
|
|
}
|
|
|
|
await page.goto("/");
|
|
const freeAction = page.getByRole("button", { name: "Create a Free account" });
|
|
await freeAction.focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(page).toHaveURL(/\/register$/);
|
|
|
|
await page.goBack();
|
|
const proAction = page.getByRole("button", { name: "Sign in to view Pro" });
|
|
await proAction.focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(page).toHaveURL(/\/login$/);
|
|
});
|
|
|
|
test("login establishes an authenticated session", async ({ page }) => {
|
|
await login(page);
|
|
await expect(page.getByText("e2e@example.test").first()).toBeVisible();
|
|
await expect(page.getByTestId("admin-version-badge")).toHaveText("ve2e-verification");
|
|
await expect(page.getByLabel("Application version e2e-verification, commit e2e1234")).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Notifications" }).click();
|
|
await expect(page.getByLabel("Notifications panel")).toBeVisible();
|
|
await expect(page).toHaveURL(/\/dashboard$/);
|
|
await page.getByRole("button", { name: "Close" }).click();
|
|
});
|
|
|
|
test("a Free account keeps manual work available while AI actions stay honestly locked", async ({ page }) => {
|
|
const suffix = Date.now().toString();
|
|
const email = `free-${suffix}@example.test`;
|
|
const password = "FreeUserPassword123!";
|
|
|
|
await login(page);
|
|
const headers = await csrfHeader(page);
|
|
const createUser = await page.request.post(`${apiUrl}/users`, {
|
|
headers,
|
|
data: { email, password, displayName: "Free E2E User", roles: [] },
|
|
});
|
|
expect(createUser.ok()).toBeTruthy();
|
|
|
|
await page.context().clearCookies();
|
|
await loginAs(page, email, password);
|
|
await expect(page.getByTestId("admin-version-badge")).toHaveCount(0);
|
|
|
|
await page.evaluate(() => window.localStorage.setItem("jobtracker.themeMode", "dark"));
|
|
await page.setViewportSize({ width: 375, height: 900 });
|
|
await page.goto("/career");
|
|
await page.getByRole("combobox", { name: "Career workspace section" }).click();
|
|
await page.getByRole("option", { name: "Import review" }).click();
|
|
await expect(page.getByText(/Build your Career Profile faster with Pro/)).toBeVisible();
|
|
await page.getByRole("button", { name: "Dismiss Build your Career Profile faster with Pro." }).click();
|
|
await expect(page.getByText(/Build your Career Profile faster with Pro/)).toHaveCount(0);
|
|
await page.getByRole("combobox", { name: "Career workspace section" }).click();
|
|
await page.getByRole("option", { name: "Career profile" }).click();
|
|
await expect(page.getByLabel("Professional headline")).toBeEnabled();
|
|
|
|
await page.goto("/settings");
|
|
await expect(page.getByText(/Free plan includes core job tracking without AI/i)).toBeVisible();
|
|
await expect(page.getByText(/Pro upgrades are not available in this deployment yet/i)).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Upgrade to Pro" })).toHaveCount(0);
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
|
|
await page.getByLabel("Settings section").click();
|
|
await page.getByRole("option", { name: "Backup" }).click();
|
|
await expect(page.getByText(/requires a sign-in from the last 15 minutes/i)).toBeVisible();
|
|
const exportResponsePromise = page.waitForResponse((response) => response.url().endsWith("/api/export/account") && response.request().method() === "POST");
|
|
await page.getByRole("button", { name: "Download readable account export" }).click();
|
|
const exportResponse = await exportResponsePromise;
|
|
expect(exportResponse.status()).toBe(200);
|
|
expect(exportResponse.headers()["content-type"]).toContain("application/zip");
|
|
expect((await exportResponse.body()).subarray(0, 2).toString()).toBe("PK");
|
|
await expect(page.getByText(/Readable account export downloaded/i)).toBeVisible();
|
|
});
|
|
|
|
test("a saved job can be created through the reviewed UI flow", async ({ page }) => {
|
|
const suffix = Date.now().toString();
|
|
const title = `E2E Engineer ${suffix}`;
|
|
|
|
await login(page);
|
|
await page.goto("/jobs");
|
|
await page.getByRole("button", { name: "Add Job" }).click();
|
|
await page.getByRole("button", { name: "Enter details manually" }).click();
|
|
await page.getByLabel("Company").fill(`E2E Company ${suffix}`);
|
|
await page.getByLabel("Job title").fill(title);
|
|
await page.getByRole("button", { name: "Continue" }).click();
|
|
for (let step = 0; step < 3; step += 1) {
|
|
await page.getByRole("button", { name: "Skip and continue" }).click();
|
|
}
|
|
await page.getByRole("button", { name: "Create job", exact: true }).click();
|
|
|
|
await expect(page.getByText(title, { exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("the dedicated application workspace survives deep links, long data and unsaved navigation", async ({ page }) => {
|
|
const suffix = Date.now().toString();
|
|
const companyName = `Application Workspace Company With A Deliberately Long Name ${suffix}`;
|
|
const title = `Principal Platform Reliability Engineer For Distributed Customer Systems ${suffix}`;
|
|
|
|
await login(page);
|
|
const headers = await csrfHeader(page);
|
|
let companyResponse = await page.request.post(`${apiUrl}/companies`, {
|
|
headers,
|
|
data: { name: companyName, location: "Oslo and remote across Europe", source: "direct" },
|
|
});
|
|
for (let attempt = 0; attempt < 2 && !companyResponse.ok(); attempt += 1) {
|
|
await page.waitForTimeout(250);
|
|
companyResponse = await page.request.post(`${apiUrl}/companies`, {
|
|
headers,
|
|
data: { name: companyName, location: "Oslo and remote across Europe", source: "direct" },
|
|
});
|
|
}
|
|
const company = await companyResponse.json();
|
|
expect(companyResponse.ok(), `company create failed: ${companyResponse.status()} ${JSON.stringify(company)}`).toBeTruthy();
|
|
|
|
const jobData = {
|
|
jobTitle: title,
|
|
companyId: company.id,
|
|
status: "Applied",
|
|
location: "Oslo and remote across Europe",
|
|
salary: null,
|
|
salaryMin: null,
|
|
salaryMax: null,
|
|
salaryCurrency: null,
|
|
salaryPeriod: null,
|
|
nextAction: "Prepare a concise application answer",
|
|
followUpAt: null,
|
|
notes: "Ask about platform ownership and the incident response rotation.",
|
|
description: `Build reliable distributed systems. ${"Long responsibility text ".repeat(80)} https://example.test/${"very-long-path-segment/".repeat(12)}`,
|
|
translatedDescription: null,
|
|
descriptionLanguage: "en",
|
|
tags: JSON.stringify([".NET", "Kubernetes", "Incident response"]),
|
|
deadline: null,
|
|
coverLetterText: null,
|
|
jobUrl: "https://example.test/jobs/platform-reliability",
|
|
dateApplied: new Date().toISOString(),
|
|
feedbackRequestedAt: null,
|
|
source: "direct",
|
|
countryCode: "NO",
|
|
};
|
|
let jobResponse = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: jobData });
|
|
for (let attempt = 0; attempt < 2 && !jobResponse.ok(); attempt += 1) {
|
|
await page.waitForTimeout(250);
|
|
jobResponse = await page.request.post(`${apiUrl}/jobapplications`, { headers, data: jobData });
|
|
}
|
|
expect(jobResponse.ok()).toBeTruthy();
|
|
const job = await jobResponse.json();
|
|
|
|
await page.goto("/jobs");
|
|
const applicationRow = page.getByRole("row", { name: new RegExp(`Open ${title}`, "i") });
|
|
await applicationRow.focus();
|
|
await page.keyboard.press("Enter");
|
|
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}$`));
|
|
await expect(page.getByRole("heading", { name: title })).toBeVisible();
|
|
|
|
await page.goBack();
|
|
await expect(page).toHaveURL(/\/jobs$/);
|
|
await page.goForward();
|
|
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}$`));
|
|
|
|
const workspaceNav = page.getByRole("tablist", { name: "Workspace sections" });
|
|
await workspaceNav.getByRole("tab", { name: "Cover Letter", exact: true }).click();
|
|
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=cover-letter$`));
|
|
await page.getByLabel("Application answer", { exact: true }).fill("A reviewed answer that must not be lost.");
|
|
await workspaceNav.getByRole("tab", { name: "Analysis", exact: true }).click();
|
|
await expect(page.getByRole("dialog", { name: "Unsaved application changes" })).toBeVisible();
|
|
await page.getByRole("button", { name: "Keep editing" }).click();
|
|
await expect(page.getByLabel("Application answer", { exact: true })).toHaveValue("A reviewed answer that must not be lost.");
|
|
await page.getByRole("button", { name: "Save application drafts" }).click();
|
|
await expect(page.getByText("Unsaved changes")).toHaveCount(0);
|
|
|
|
await page.evaluate(() => window.localStorage.setItem("jobtracker.themeMode", "light"));
|
|
await page.reload();
|
|
for (const width of [375, 768, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await expect(page.getByRole("heading", { name: title })).toBeVisible();
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
}
|
|
|
|
await page.evaluate(() => window.localStorage.setItem("jobtracker.themeMode", "dark"));
|
|
await page.reload();
|
|
await expect(page.getByLabel("Application answer", { exact: true })).toHaveValue("A reviewed answer that must not be lost.");
|
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark");
|
|
|
|
for (const width of [375, 768, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await expect(page.getByRole("heading", { name: title })).toBeVisible();
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
}
|
|
|
|
await workspaceNav.getByRole("tab", { name: "Overview", exact: true }).click();
|
|
await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=overview$`));
|
|
await page.getByRole("button", { name: "Job details", exact: true }).click();
|
|
await expect(page.getByText("Ask about platform ownership and the incident response rotation.")).toBeVisible();
|
|
await expect(page.getByText("<<<APPLICATION_ANSWER_DRAFT>>>")).toHaveCount(0);
|
|
|
|
await page.getByRole("button", { name: "Back to applications" }).click();
|
|
await expect(page).toHaveURL(/\/jobs$/);
|
|
await expect(page.getByRole("row", { name: new RegExp(`Open ${title}`, "i") })).toBeFocused();
|
|
|
|
await page.goto("/jobs/2147483647");
|
|
const missingJobAlert = page.getByRole("alert").filter({ hasText: /Not Found|Could not open this application/i });
|
|
await expect(missingJobAlert).toBeVisible();
|
|
const alertColours = await missingJobAlert.evaluate((element) => {
|
|
const styles = window.getComputedStyle(element);
|
|
const parentStyles = window.getComputedStyle(element.parentElement!);
|
|
return {
|
|
foreground: styles.color,
|
|
background: styles.backgroundColor,
|
|
parentBackground: parentStyles.backgroundColor,
|
|
};
|
|
});
|
|
const alertBackground = composite(parseCssColour(alertColours.background), parseCssColour(alertColours.parentBackground));
|
|
expect(contrastRatio(parseCssColour(alertColours.foreground), alertBackground)).toBeGreaterThanOrEqual(4.5);
|
|
await expect(page.getByRole("button", { name: "Back to applications" })).toBeVisible();
|
|
});
|
|
|
|
test("Career Workspace loads from the authenticated application shell", async ({ page }) => {
|
|
await login(page);
|
|
await page.evaluate(() => window.localStorage.setItem("jobtracker.themeMode", "light"));
|
|
await page.goto("/career");
|
|
await expect(page.getByRole("heading", { name: "Career Workspace" })).toBeVisible();
|
|
await expect(page.getByRole("link", { name: "Open CV Builder" })).toHaveAttribute("href", "/career/builder");
|
|
|
|
await page.setViewportSize({ width: 375, height: 900 });
|
|
const careerSection = page.getByRole("combobox", { name: "Career workspace section" });
|
|
await expect(careerSection).toBeVisible();
|
|
await careerSection.click();
|
|
await page.getByRole("option", { name: "Career profile" }).click();
|
|
await expect(page).toHaveURL(/\/career\?section=profile$/);
|
|
await expect(page.getByRole("heading", { name: "Career profile" })).toBeVisible();
|
|
await careerSection.click();
|
|
await page.getByRole("option", { name: "Import review" }).click();
|
|
await expect(page).toHaveURL(/\/career\?section=import$/);
|
|
await expect(page.getByRole("heading", { name: "Master CV" })).toBeVisible();
|
|
await careerSection.click();
|
|
await page.getByRole("option", { name: "Overview" }).click();
|
|
|
|
for (const width of [375, 768, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
}
|
|
|
|
await page.getByRole("link", { name: "Open CV Builder" }).click();
|
|
await expect(page).toHaveURL(/\/career\/builder$/);
|
|
await page.getByRole("button", { name: "New CV" }).first().click();
|
|
await expect(page.getByRole("dialog", { name: "Create a CV" })).toBeVisible();
|
|
await page.getByRole("button", { name: "Create CV" }).click();
|
|
await expect(page).toHaveURL(/\/career\/builder\/\d+$/);
|
|
await expect(page.getByLabel("CV name")).toBeVisible();
|
|
|
|
await page.getByRole("tab", { name: "Content" }).click();
|
|
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.getByRole("button", { name: "Add content" }).click();
|
|
const addContentDialog = page.getByRole("dialog", { name: "Add content" });
|
|
const additionalExperienceCard = addContentDialog.getByRole("heading", { name: "Additional Experience" }).locator("..");
|
|
await additionalExperienceCard.getByRole("button", { name: "Add", exact: true }).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: "Customize" }).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 Tools" })).toBeVisible();
|
|
|
|
await page.evaluate(() => window.localStorage.setItem("jobtracker.themeMode", "dark"));
|
|
await page.reload();
|
|
await expect(page.locator("html")).toHaveAttribute("data-color-scheme", "dark");
|
|
for (const width of [375, 768, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await expect(page.getByLabel("CV name")).toBeVisible();
|
|
if (width === 375) {
|
|
await page.getByRole("button", { name: "Preview" }).click();
|
|
await expect(page.getByTitle("CV preview page 1")).toBeVisible();
|
|
await page.getByRole("button", { name: "Edit" }).click();
|
|
}
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
}
|
|
});
|
|
|
|
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`, {
|
|
headers,
|
|
data: { name: "E2E Public CV" },
|
|
});
|
|
expect(create.ok()).toBeTruthy();
|
|
const variant = await create.json();
|
|
|
|
const publish = await page.request.put(`${apiUrl}/cv/variants/${variant.id}/public`, {
|
|
headers,
|
|
data: { isPublic: true },
|
|
});
|
|
expect(publish.ok()).toBeTruthy();
|
|
const published = await publish.json();
|
|
|
|
await page.context().clearCookies();
|
|
await page.setViewportSize({ width: 375, height: 900 });
|
|
await page.goto(`/cv/${published.publicSlug}`);
|
|
const publicCvFrame = page.getByTitle("Public CV");
|
|
await expect(publicCvFrame).toBeVisible();
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
const publicCvMetrics = await publicCvFrame.evaluate((element) => {
|
|
const frame = element as HTMLIFrameElement;
|
|
return {
|
|
renderedWidth: frame.getBoundingClientRect().width,
|
|
innerOverflow: (frame.contentDocument?.documentElement.scrollWidth ?? 0) - (frame.contentDocument?.documentElement.clientWidth ?? 0),
|
|
};
|
|
});
|
|
expect(publicCvMetrics.renderedWidth).toBeLessThanOrEqual(343);
|
|
expect(publicCvMetrics.innerOverflow).toBeLessThanOrEqual(1);
|
|
await expect(page.getByRole("link", { name: "Download PDF" })).toHaveAttribute("href", /\/pdf$/);
|
|
const pdf = await page.request.get(`${apiUrl}/public-cv/${published.publicSlug}/pdf`);
|
|
expect(pdf.ok()).toBeTruthy();
|
|
expect(pdf.headers()["content-type"]).toContain("application/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.getByRole("tab", { name: "Template" })).toHaveAttribute("aria-selected", "true");
|
|
await page.getByRole("tab", { name: "Content" }).click();
|
|
await page.getByRole("button", { name: "Expand Experience" }).click();
|
|
await page.getByRole("button", { name: "Expand Education" }).click();
|
|
await expect(page.getByRole("button", { name: "Collapse Experience" })).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Collapse Education" })).toBeVisible();
|
|
|
|
const addContentButton = page.getByRole("button", { name: "Add content" });
|
|
await addContentButton.focus();
|
|
await addContentButton.click();
|
|
const addContentDialog = page.getByRole("dialog", { name: "Add content" });
|
|
await expect(addContentDialog).toBeVisible();
|
|
await expect(addContentDialog.getByRole("heading", { name: "Additional Experience" })).toBeVisible();
|
|
await addContentDialog.getByRole("button", { name: "Cancel" }).click();
|
|
await expect(addContentDialog).toBeHidden();
|
|
await expect(addContentButton).toBeFocused();
|
|
|
|
await page.getByRole("tab", { name: "Customize" }).click();
|
|
await expect(page.getByLabel("Page size")).toBeVisible();
|
|
await expect(page.getByLabel("Skills presentation")).toBeVisible();
|
|
await page.getByRole("tab", { name: "AI Tools" }).click();
|
|
await expect(page.getByText("Translate resume", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("Check spelling and grammar", { exact: true })).toBeVisible();
|
|
|
|
for (const scheme of ["light", "dark"] as const) {
|
|
await page.evaluate((value) => window.localStorage.setItem("jobtracker.themeMode", value), scheme);
|
|
await page.reload();
|
|
for (const width of [375, 768, 1440]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
|
expect(overflow).toBeLessThanOrEqual(1);
|
|
}
|
|
}
|
|
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" });
|
|
});
|