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 login(page: Page) { await page.goto("/login"); await page.getByLabel("Email").fill("e2e@example.test"); await page.getByLabel("Current password").fill("E2ePassword123!"); 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 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("login establishes an authenticated session", async ({ page }) => { await login(page); await expect(page.getByText("e2e@example.test").first()).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("navigation", { name: "Workspace sections" }); await workspaceNav.getByRole("button", { name: "Cover Letter", exact: true }).click(); await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=cover-letter$`)); await page.getByLabel("Application answer").fill("A reviewed answer that must not be lost."); await workspaceNav.getByRole("button", { name: "Match", 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")).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")).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("button", { name: "Job Details", exact: true }).click(); await expect(page).toHaveURL(new RegExp(`/jobs/${job.id}\\?section=job-details$`)); await expect(page.getByText("Ask about platform ownership and the incident response rotation.")).toBeVisible(); await expect(page.getByText("<<>>")).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.goto("/career"); await expect(page.getByRole("heading", { name: "Career Workspace" })).toBeVisible(); await expect(page.getByRole("link", { name: "Open CV Builder" })).toHaveAttribute("href", "/career/builder"); }); test("a public CV renders anonymously and downloads as PDF", async ({ page }) => { 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"); expect((await pdf.body()).subarray(0, 5).toString()).toBe("%PDF-"); });