fix: close final portfolio quality gaps

This commit is contained in:
2026-09-02 17:38:11 +03:00
parent 0899b6b00f
commit b4728ceef0
24 changed files with 1822 additions and 889 deletions

View File

@@ -1,49 +1,50 @@
import type { PortfolioContent } from '@/content/types';
import { expect, expectInsetFocusIndicator, test } from '../helpers/serve-dist';
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',
"start",
"profile",
"experience",
"infrastructure",
"selected-builds",
"additional-work",
"toolkit",
"contact",
] as const;
const sectionHeadings = [
'Profile',
'Experience',
'Infrastructure',
'Selected builds',
'Additional work',
'Toolkit',
'Contact',
"Profile",
"Experience",
"Infrastructure",
"Selected builds",
"Additional work",
"Toolkit",
"Contact",
] as const;
const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> = {
start: 'Start',
profile: 'Profile',
experience: 'Career',
infrastructure: 'Infra',
'selected-builds': 'Builds',
'additional-work': 'More work',
toolkit: 'Toolkit',
contact: 'Contact',
};
const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> =
{
start: "Start",
profile: "Profile",
experience: "Career",
infrastructure: "Infra",
"selected-builds": "Builds",
"additional-work": "More work",
toolkit: "Toolkit",
contact: "Contact",
};
const visibleTextLeafSelector = 'h1,h2,h3,p,dt,dd,li,figcaption,a,svg text';
const visibleTextLeafSelector = "h1,h2,h3,p,dt,dd,li,figcaption,a,svg text";
const visibleTextSelector = [
'.site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)',
'.portfolio-main :is(h1,h2,h3,p,dt,dd,li,figcaption,a)',
'.portfolio-main svg text',
].join(',');
".site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)",
".portfolio-main :is(h1,h2,h3,p,dt,dd,li,figcaption,a)",
".portfolio-main svg text",
].join(",");
const normalizedText = (value: string): string =>
value.replace(/\s+/gu, ' ').trim();
value.replace(/\s+/gu, " ").trim();
type TextMultiset = Array<[text: string, count: number]>;
@@ -69,14 +70,14 @@ const missingText = (
};
const portfolioContentFromSnapshot = async (
page: import('@playwright/test').Page,
page: import("@playwright/test").Page,
rawHtml: string,
): Promise<PortfolioContent> =>
page.evaluate((html) => {
const document = new DOMParser().parseFromString(html, 'text/html');
const snapshot = document.querySelector('#portfolio-inputs');
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');
throw new Error("Missing #portfolio-inputs snapshot");
}
return (JSON.parse(snapshot.textContent) as { content: PortfolioContent })
.content;
@@ -89,7 +90,7 @@ const expectedVisibleTextSegments = (
content.navigation.map((item) => [item.id, item.label]),
);
const section = (
id: Exclude<(typeof sectionIds)[number], 'start'>,
id: Exclude<(typeof sectionIds)[number], "start">,
body: readonly string[],
eyebrow?: string,
): string[] => {
@@ -100,60 +101,65 @@ const expectedVisibleTextSegments = (
);
};
const projectLinkText = (
project: PortfolioContent['projects']['selected'][number],
project: PortfolioContent["projects"]["selected"][number],
): string[] =>
project.visibility === 'public' && project.url ? [project.title] : [];
project.visibility === "public" && project.url ? [project.title] : [];
return [
...sectionIds.map((id) => navigationLabels[id]),
content.identity.name,
content.identity.role,
content.identity.valueProposition,
'Explore work',
...section('profile', [
"Explore work",
...section("profile", [
content.profile.introduction,
...content.profile.screeningFacts.flatMap((fact) => [
fact.label,
fact.value,
]),
]),
...section('experience', [
...section("experience", [
...content.experience.flatMap((record) => [
record.role,
`${record.organization} · ${record.dates}${record.location === undefined ? '' : ` · ${record.location}`}`,
`${record.organization} · ${record.dates}${record.location === undefined ? "" : ` · ${record.location}`}`,
...record.highlights,
]),
content.systemsOperations.title,
content.systemsOperations.summary,
...content.systemsOperations.evidence,
]),
...section('infrastructure', [
...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}`,
),
]),
...section(
'selected-builds',
"selected-builds",
content.projects.selected.flatMap((project) => [
project.title,
project.summary,
'Problem',
"Problem",
project.problem,
'Contribution',
"Contribution",
project.contribution,
'Technical decisions',
"Technical decisions",
...project.technicalDecisions,
'Outcome',
"Outcome",
project.outcome,
...projectLinkText(project),
]),
'Selected work',
"Selected work",
),
...section(
'additional-work',
"additional-work",
content.projects.additional.flatMap((project) => [
project.title,
project.summary,
@@ -163,29 +169,33 @@ const expectedVisibleTextSegments = (
]),
),
...section(
'toolkit',
"toolkit",
content.toolkit.flatMap((group) => [
group.title,
...group.technologies.map((technology) => technology.name),
]),
),
...section('contact', [
"Thanks for making the trip. The road's open if you'd like to build something together.",
'Start a conversation',
content.contact.email,
content.contact.cvMessage,
...content.contact.social.map((link) => link.service),
], 'Destination'),
...section(
"contact",
[
"Thanks for making the trip. The road's open if you'd like to build something together.",
"Start a conversation",
content.contact.email,
content.contact.cvMessage,
...content.contact.social.map((link) => link.service),
],
"Destination",
),
];
};
const rawVisibleTextSegments = async (
page: import('@playwright/test').Page,
page: import("@playwright/test").Page,
rawHtml: string,
): Promise<string[]> =>
page.evaluate(
({ html, leafSelector, rootSelector }) => {
const document = new DOMParser().parseFromString(html, 'text/html');
const document = new DOMParser().parseFromString(html, "text/html");
const isHidden = (element: Element): boolean => {
for (
let current: Element | null = element;
@@ -193,21 +203,21 @@ const rawVisibleTextSegments = async (
current = current.parentElement
) {
if (
current.matches('template,script,style,noscript') ||
current.hasAttribute('hidden') ||
current.hasAttribute('inert')
current.matches("template,script,style,noscript") ||
current.hasAttribute("hidden") ||
current.hasAttribute("inert")
) {
return true;
}
const ariaHidden = current.getAttribute('aria-hidden');
const ariaHidden = current.getAttribute("aria-hidden");
if (
ariaHidden !== null &&
ariaHidden.trim().toLowerCase() !== 'false'
ariaHidden.trim().toLowerCase() !== "false"
) {
return true;
}
const hiddenClass =
current.getAttribute('class')?.toLowerCase() ?? '';
current.getAttribute("class")?.toLowerCase() ?? "";
if (
/(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test(
hiddenClass,
@@ -216,7 +226,7 @@ const rawVisibleTextSegments = async (
return true;
}
const inlineStyle =
current.getAttribute('style')?.toLowerCase() ?? '';
current.getAttribute("style")?.toLowerCase() ?? "";
if (
/(?:^|;)\s*display\s*:\s*none\b/u.test(inlineStyle) ||
/(?:^|;)\s*visibility\s*:\s*hidden\b/u.test(inlineStyle) ||
@@ -233,7 +243,7 @@ const rawVisibleTextSegments = async (
!isHidden(element) && element.querySelector(leafSelector) === null,
)
.map((element) =>
(element.textContent ?? '').replace(/\s+/gu, ' ').trim(),
(element.textContent ?? "").replace(/\s+/gu, " ").trim(),
)
.filter((text) => text.length > 0);
},
@@ -245,7 +255,7 @@ const rawVisibleTextSegments = async (
);
const browserVisibleTextSegments = async (
page: import('@playwright/test').Page,
page: import("@playwright/test").Page,
): Promise<string[]> =>
page.locator(visibleTextSelector).evaluateAll(
(elements, leafSelector) =>
@@ -257,21 +267,21 @@ const browserVisibleTextSegments = async (
current = current.parentElement
) {
if (
current.matches('template,script,style,noscript') ||
current.hasAttribute('hidden') ||
current.hasAttribute('inert')
current.matches("template,script,style,noscript") ||
current.hasAttribute("hidden") ||
current.hasAttribute("inert")
) {
return false;
}
const ariaHidden = current.getAttribute('aria-hidden');
const ariaHidden = current.getAttribute("aria-hidden");
if (
ariaHidden !== null &&
ariaHidden.trim().toLowerCase() !== 'false'
ariaHidden.trim().toLowerCase() !== "false"
) {
return false;
}
const hiddenClass =
current.getAttribute('class')?.toLowerCase() ?? '';
current.getAttribute("class")?.toLowerCase() ?? "";
if (
/(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test(
hiddenClass,
@@ -281,9 +291,9 @@ const browserVisibleTextSegments = async (
}
const style = getComputedStyle(current);
if (
style.display === 'none' ||
style.visibility === 'hidden' ||
style.opacity === '0'
style.display === "none" ||
style.visibility === "hidden" ||
style.opacity === "0"
) {
return false;
}
@@ -294,13 +304,13 @@ const browserVisibleTextSegments = async (
);
})
.map((element) =>
(element.textContent ?? '').replace(/\s+/gu, ' ').trim(),
(element.textContent ?? "").replace(/\s+/gu, " ").trim(),
)
.filter((text) => text.length > 0),
visibleTextLeafSelector,
);
test('keeps every raw SSR reading-path segment visible without JavaScript', async ({
test("keeps every raw SSR reading-path segment visible without JavaScript", async ({
page,
distUrl,
request,
@@ -308,14 +318,14 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
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("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') {
page.on("requestfailed", (request) => {
if (request.resourceType() !== "script") {
failedRequests.push(
`${request.url()}: ${request.failure()?.errorText ?? 'failed'}`,
`${request.url()}: ${request.failure()?.errorText ?? "failed"}`,
);
}
});
@@ -331,7 +341,7 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
await rawVisibleTextSegments(page, rawHtml),
);
await page.goto(distUrl, { waitUntil: 'load' });
await page.goto(distUrl, { waitUntil: "load" });
const actualVisibleText = textMultiset(
await browserVisibleTextSegments(page),
);
@@ -340,37 +350,37 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
expect(rawVisibleText).toEqual(expectedVisibleText);
expect(actualVisibleText).toEqual(expectedVisibleText);
expect(normalizedText(JSON.stringify(actualVisibleText))).not.toContain(
'preview-semantic-v1',
"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("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("main")).toBeVisible();
await expect(
page.locator('nav[aria-label="Portfolio sections"]'),
).toBeVisible();
await expect(page.locator('footer')).toBeVisible();
await expect(page.locator("footer")).toBeVisible();
expect(
await page
.locator('main > section')
.locator("main > section")
.evaluateAll((sections) => sections.map((section) => section.id)),
).toEqual(sectionIds);
expect(await page.locator('main > section > h2').allTextContents()).toEqual(
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',
"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(),
@@ -379,30 +389,30 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
const email = page.locator('a[href="mailto:luca.pislaru@gmail.com"]');
await expect(email).toBeVisible();
await expect(email).toHaveText('Start a conversation');
await expect(email).toHaveText("Start a conversation");
await expect(
page.getByText(
'For a current CV or role-specific information, contact me directly.',
"For a current CV or role-specific information, contact me directly.",
{ exact: true },
),
).toBeVisible();
for (const title of [
'Private self-hosted medication tracker',
'Private healthcare platform',
'Private game prototype',
"Private self-hosted medication tracker",
"Private healthcare platform",
"Private game prototype",
]) {
await expect(
page.locator('article').filter({ hasText: title }).locator('a'),
page.locator("article").filter({ hasText: title }).locator("a"),
).toHaveCount(0);
}
await expect(page.locator('a[download]')).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 ({
test("content oracle detects a removed highlight and an inert reading path", async ({
page,
distUrl,
request,
@@ -417,31 +427,31 @@ test('content oracle detects a removed highlight and an inert reading path', asy
const withoutHighlight = await page.evaluate(
({ html, highlight }) => {
const document = new DOMParser().parseFromString(html, 'text/html');
const target = [...document.querySelectorAll('li')].find(
const document = new DOMParser().parseFromString(html, "text/html");
const target = [...document.querySelectorAll("li")].find(
(element) =>
(element.textContent ?? '').replace(/\s+/gu, ' ').trim() ===
(element.textContent ?? "").replace(/\s+/gu, " ").trim() ===
highlight,
);
if (target === undefined) throw new Error('Expected highlight is absent');
if (target === undefined) throw new Error("Expected highlight is absent");
target.remove();
return `<!doctype html>${document.documentElement.outerHTML}`;
},
{ html: rawHtml, highlight: firstHighlight ?? '' },
{ 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(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', '');
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 `<!doctype html>${document.documentElement.outerHTML}`;
}, rawHtml);
const inertMainText = textMultiset(
@@ -457,33 +467,33 @@ test('content oracle detects a removed highlight and an inert reading path', asy
expect(missingText(inertMainText, expected)).toEqual([]);
});
test('activating the skip link focuses visible portfolio content without JavaScript', async ({
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.goto(distUrl, { waitUntil: "load" });
await page.keyboard.press("Tab");
await expect(page.locator(".skip-link")).toBeFocused();
await page.keyboard.press('Enter');
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 expect(page.locator("#portfolio-content")).toBeFocused();
await expect(page.locator("#portfolio-content")).toHaveAttribute(
"tabindex",
"-1",
);
await expectInsetFocusIndicator(page, '#portfolio-content');
await expectInsetFocusIndicator(page, "#portfolio-content");
});
test('retains native section navigation when scripts are disabled', async ({
test("retains native section navigation when scripts are disabled", async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: 'load' });
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")).toBeInViewport();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
});