import type { PortfolioContent } from "@/content/types"; import { expect, expectInsetFocusIndicator, test } from "../helpers/serve-dist"; test.use({ javaScriptEnabled: false }); const sectionIds = [ "start", "profile", "experience", "infrastructure", "selected-builds", "additional-work", "toolkit", "contact", ] as const; const sectionHeadings = [ "Profile", "Experience", "Infrastructure", "Selected builds", "Additional work", "Toolkit", "Contact", ] as const; const navigationLabels: Readonly> = { start: "Start", profile: "Profile", experience: "Career", infrastructure: "Infra", "selected-builds": "Builds", "additional-work": "More work", toolkit: "Toolkit", contact: "Contact", }; const visibleTextLeafSelector = "h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a,summary,svg text"; const visibleTextSelector = [ ".site-header :is(h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a)", ".portfolio-main :is(h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a,summary)", ".portfolio-main svg text", ].join(","); const normalizedText = (value: string): string => value.replace(/\s+/gu, " ").trim(); type TextMultiset = Array<[text: string, count: number]>; const textMultiset = (values: readonly string[]): TextMultiset => { const counts = new Map(); for (const value of values.map(normalizedText).filter(Boolean)) { counts.set(value, (counts.get(value) ?? 0) + 1); } return [...counts].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0, ); }; const missingText = ( expected: TextMultiset, actual: TextMultiset, ): TextMultiset => { const actualCounts = new Map(actual); return expected.flatMap(([text, count]) => { const missing = count - (actualCounts.get(text) ?? 0); return missing > 0 ? [[text, missing] as [string, number]] : []; }); }; const portfolioContentFromSnapshot = async ( page: import("@playwright/test").Page, rawHtml: string, ): Promise => page.evaluate((html) => { const document = new DOMParser().parseFromString(html, "text/html"); const snapshot = document.querySelector("#portfolio-inputs"); if (snapshot?.textContent === null || snapshot?.textContent === undefined) { throw new Error("Missing #portfolio-inputs snapshot"); } return (JSON.parse(snapshot.textContent) as { content: PortfolioContent }) .content; }, rawHtml); const expectedVisibleTextSegments = ( content: Readonly, ): string[] => { const navigationById = new Map( content.navigation.map((item) => [item.id, item.label]), ); const section = ( id: Exclude<(typeof sectionIds)[number], "start">, body: readonly string[], eyebrow?: string, ): string[] => { const label = navigationById.get(id); if (label === undefined) throw new Error(`Missing navigation label: ${id}`); return [eyebrow, label, ...body].filter( (segment): segment is string => segment !== undefined, ); }; const projectLinkText = ( project: PortfolioContent["projects"]["selected"][number], ): string[] => project.visibility === "public" && project.url ? [project.title] : []; const projectMediaText = ( project: PortfolioContent["projects"]["selected"][number], ): string[] => (project.media ?? []).flatMap((presentation) => [ presentation.caption, presentation.longDescription, ...(presentation.kind === "video" ? ["Open gameplay video"] : []), ...(presentation.kind === "chart" ? [ "Variables", presentation.chart.variables, "Trend", presentation.chart.trend, "Conclusion", presentation.chart.conclusion, ...(presentation.chart.table === undefined ? [] : [ presentation.chart.table.caption, ...presentation.chart.table.columns, ...presentation.chart.table.rows.flat(), ]), ] : []), ].filter((segment): segment is string => segment !== undefined)); return [ ...sectionIds.map((id) => navigationLabels[id]), content.identity.name, content.identity.role, content.identity.valueProposition, content.identity.availability, "Contact Me", "Explore work", ...section("profile", [ content.profile.introduction, ...content.profile.screeningFacts.flatMap((fact) => [ fact.label, fact.value, ]), ]), ...section("experience", [ ...content.experience.flatMap((record) => [ record.role, `${record.organization} · ${record.dates}${record.location === undefined ? "" : ` · ${record.location}`}`, ...record.highlights, ]), content.systemsOperations.title, content.systemsOperations.summary, ...content.systemsOperations.evidence, ]), ...section("infrastructure", [ content.infrastructure.title, content.infrastructure.summary, content.infrastructure.title, "Trust boundary", "Where trust or access rules change", "Request flow", "The direction traffic moves through the system", "Legend: Boundary labels mark changes in trust or access rules. Connectors show request flow direction.", ...content.infrastructure.bands.map( (band) => `${band.title}: ${band.summary}`, ), content.infrastructure.featuredOperation.title, content.infrastructure.featuredOperation.context, ...content.infrastructure.featuredOperation.actions, `Outcome: ${content.infrastructure.featuredOperation.outcome}`, ]), ...section( "selected-builds", content.projects.selected.flatMap((project) => [ project.title, project.summary, ...projectMediaText(project), "Problem", project.problem, "Contribution", project.contribution, "Technical decisions", ...project.technicalDecisions, "Outcome", project.outcome, ...projectLinkText(project), ]), "Selected work", ), ...section( "additional-work", content.projects.additional.flatMap((project) => [ project.title, project.summary, ...projectMediaText(project), project.contribution, ...projectLinkText(project), ]), ), ...section( "toolkit", content.toolkit.flatMap((group) => [ group.title, ...group.technologies.map((technology) => technology.name), ]), ), ...section( "contact", [ content.contact.introduction, "Email Me", content.contact.cvMessage, ...content.contact.social.map((link) => link.service), ], "Destination", ), ]; }; const rawVisibleTextSegments = async ( page: import("@playwright/test").Page, rawHtml: string, ): Promise => page.evaluate( ({ html, leafSelector, rootSelector }) => { const document = new DOMParser().parseFromString(html, "text/html"); const isHidden = (element: Element): boolean => { if (element.closest('.project-media--video[data-enhanced="false"] .video-controls')) return true; for ( let current: Element | null = element; current; current = current.parentElement ) { if ( current.matches("template,script,style,noscript") || current.hasAttribute("hidden") || current.hasAttribute("inert") ) { return true; } const ariaHidden = current.getAttribute("aria-hidden"); if ( ariaHidden !== null && ariaHidden.trim().toLowerCase() !== "false" ) { return true; } const hiddenClass = current.getAttribute("class")?.toLowerCase() ?? ""; if ( /(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test( hiddenClass, ) ) { return true; } const inlineStyle = current.getAttribute("style")?.toLowerCase() ?? ""; if ( /(?:^|;)\s*display\s*:\s*none\b/u.test(inlineStyle) || /(?:^|;)\s*visibility\s*:\s*hidden\b/u.test(inlineStyle) || /(?:^|;)\s*opacity\s*:\s*0(?:[;\s]|$)/u.test(inlineStyle) ) { return true; } } return false; }; return [...document.querySelectorAll(rootSelector)] .filter( (element) => !isHidden(element) && element.querySelector(leafSelector) === null, ) .map((element) => (element.textContent ?? "").replace(/\s+/gu, " ").trim(), ) .filter((text) => text.length > 0); }, { html: rawHtml, leafSelector: visibleTextLeafSelector, rootSelector: visibleTextSelector, }, ); const browserVisibleTextSegments = async ( page: import("@playwright/test").Page, ): Promise => page.locator(visibleTextSelector).evaluateAll( (elements, leafSelector) => elements .filter((element) => { for ( let current: Element | null = element; current; current = current.parentElement ) { if ( current.matches("template,script,style,noscript") || current.hasAttribute("hidden") || current.hasAttribute("inert") ) { return false; } const ariaHidden = current.getAttribute("aria-hidden"); if ( ariaHidden !== null && ariaHidden.trim().toLowerCase() !== "false" ) { return false; } const hiddenClass = current.getAttribute("class")?.toLowerCase() ?? ""; if ( /(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test( hiddenClass, ) ) { return false; } const style = getComputedStyle(current); if ( style.display === "none" || style.visibility === "hidden" || style.opacity === "0" ) { return false; } } return ( element.querySelector(leafSelector) === null && element.getClientRects().length > 0 ); }) .map((element) => (element.textContent ?? "").replace(/\s+/gu, " ").trim(), ) .filter((text) => text.length > 0), visibleTextLeafSelector, ); test("keeps every raw SSR reading-path segment visible without JavaScript", async ({ page, distUrl, request, }) => { const pageErrors: string[] = []; const consoleErrors: string[] = []; const failedRequests: string[] = []; page.on("pageerror", (error) => pageErrors.push(error.message)); page.on("console", (message) => { if (message.type() === "error") consoleErrors.push(message.text()); }); page.on("requestfailed", (request) => { if (request.resourceType() !== "script") { failedRequests.push( `${request.url()}: ${request.failure()?.errorText ?? "failed"}`, ); } }); const rawResponse = await request.get(`${distUrl}/`); expect(rawResponse.status()).toBe(200); const rawHtml = await rawResponse.text(); const content = await portfolioContentFromSnapshot(page, rawHtml); const expectedVisibleText = textMultiset( expectedVisibleTextSegments(content), ); const rawVisibleText = textMultiset( await rawVisibleTextSegments(page, rawHtml), ); await page.goto(distUrl, { waitUntil: "load" }); const actualVisibleText = textMultiset( await browserVisibleTextSegments(page), ); expect(expectedVisibleText.length).toBeGreaterThan(80); expect(rawVisibleText).toEqual(expectedVisibleText); expect(actualVisibleText).toEqual(expectedVisibleText); expect(normalizedText(JSON.stringify(actualVisibleText))).not.toContain( "preview-semantic-v1", ); await expect(page.locator("h1:visible")).toHaveCount(1); await expect(page.locator("h1")).toHaveText("Luca Pîslaru"); await expect(page.locator(".hero-journey__role")).toHaveText( "System Administrator & Full-Stack Developer", ); await expect(page.locator("main")).toBeVisible(); await expect( page.locator('nav[aria-label="Portfolio sections"]'), ).toBeVisible(); await expect(page.locator("footer")).toBeVisible(); expect( await page .locator("main > section") .evaluateAll((sections) => sections.map((section) => section.id)), ).toEqual(sectionIds); expect(await page.locator("main > section > h2").allTextContents()).toEqual( sectionHeadings, ); for (const evidence of [ "Professional systems operations", "Secure self-hosted platform and lifecycle", "Private self-hosted medication tracker", "Footy Tracker", "Linux", "Docker", "Python", "Vue.js", ]) { await expect( page.getByText(evidence, { exact: true }).first(), ).toBeVisible(); } await expect(page.locator('a[href^="mailto:"]')).toHaveCount(0); const email = page.locator('#contact-email-details'); await email.locator('summary').click(); await expect(email.locator('.contact-email__address')).toBeVisible(); await expect(email.locator('.contact-email__address')).toHaveText('luca.pislaru [at] gmail [dot] com'); await expect( page.getByText( "Get in touch for my CV or to talk about a role.", { exact: true }, ), ).toBeVisible(); for (const title of [ "Private self-hosted medication tracker", "Private healthcare platform", "Private game prototype", ]) { await expect( page.locator("article").filter({ hasText: title }).locator("a"), ).toHaveCount(0); } await expect(page.locator("a[download]")).toHaveCount(0); expect(pageErrors).toEqual([]); expect(consoleErrors).toEqual([]); expect(failedRequests).toEqual([]); }); test("content oracle detects a removed highlight and an inert reading path", async ({ page, distUrl, request, }) => { const rawResponse = await request.get(`${distUrl}/`); expect(rawResponse.status()).toBe(200); const rawHtml = await rawResponse.text(); const content = await portfolioContentFromSnapshot(page, rawHtml); const expected = textMultiset(expectedVisibleTextSegments(content)); const firstHighlight = content.experience[0]?.highlights[0]; expect(firstHighlight).toBeTruthy(); const withoutHighlight = await page.evaluate( ({ html, highlight }) => { const document = new DOMParser().parseFromString(html, "text/html"); const target = [...document.querySelectorAll("li")].find( (element) => (element.textContent ?? "").replace(/\s+/gu, " ").trim() === highlight, ); if (target === undefined) throw new Error("Expected highlight is absent"); target.remove(); return `${document.documentElement.outerHTML}`; }, { html: rawHtml, highlight: firstHighlight ?? "" }, ); const withoutHighlightText = textMultiset( await rawVisibleTextSegments(page, withoutHighlight), ); expect(withoutHighlightText).not.toEqual(expected); const missingHighlight = missingText(expected, withoutHighlightText); expect(missingHighlight).toEqual([[normalizedText(firstHighlight ?? ""), 1]]); expect(missingText(withoutHighlightText, expected)).toEqual([]); const inertMain = await page.evaluate((html) => { const document = new DOMParser().parseFromString(html, "text/html"); const main = document.querySelector("main"); if (main === null) throw new Error("Expected main is absent"); main.setAttribute("inert", ""); return `${document.documentElement.outerHTML}`; }, rawHtml); const inertMainText = textMultiset( await rawVisibleTextSegments(page, inertMain), ); expect(inertMainText).not.toEqual(expected); const missingWhenInert = missingText(expected, inertMainText); expect(missingWhenInert.length).toBeGreaterThan(80); expect(missingWhenInert).toContainEqual([ normalizedText(content.identity.valueProposition), 1, ]); expect(missingText(inertMainText, expected)).toEqual([]); }); test("activating the skip link focuses visible portfolio content without JavaScript", async ({ page, distUrl, }) => { await page.goto(distUrl, { waitUntil: "load" }); await page.keyboard.press("Tab"); await expect(page.locator(".skip-link")).toBeFocused(); await page.keyboard.press("Enter"); await expect(page).toHaveURL(`${distUrl}/#portfolio-content`); await expect(page.locator("#portfolio-content")).toBeFocused(); await expect(page.locator("#portfolio-content")).toHaveAttribute( "tabindex", "-1", ); await expectInsetFocusIndicator(page, "#portfolio-content"); }); test("retains native section navigation when scripts are disabled", async ({ page, distUrl, }) => { await page.goto(distUrl, { waitUntil: "load" }); await page.locator('nav a[href="#contact"]').click(); await expect(page).toHaveURL(`${distUrl}/#contact`); await expect(page.locator("#contact")).toBeInViewport(); await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible(); });