feat: refine portfolio content, contact, and supporting media

This commit is contained in:
2026-09-05 20:08:57 +03:00
parent 15bcdf7cec
commit 103427f95b
53 changed files with 824 additions and 204 deletions

View File

@@ -79,7 +79,7 @@ async function enterState(
for (const link of links) link.closest("li")?.remove();
});
await expect(page.locator('#contact a[target="_blank"]')).toHaveCount(0);
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
}
}

View File

@@ -47,7 +47,7 @@ test("forced colors preserves visible focus, controls, content, and the Contact
await clickNavigationLink(page, "#contact");
await expect(page.locator("#contact-heading")).toBeFocused();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
expect(
await page.evaluate(
() =>

View File

@@ -151,7 +151,7 @@ async function assertReflowGeometry(page: Page): Promise<void> {
await clickNavigationLink(page, "#contact");
await expect(page.locator("#contact-heading")).toBeFocused();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
}
test("reflows at a 320 CSS pixel mobile viewport without lost content or controls", async ({

View File

@@ -0,0 +1,96 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, test } from '../helpers/serve-dist';
test('start navigation, contact identity, and toolkit icons use the approved content', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
const response = await page.goto(distUrl);
const html = await response!.text();
expect(html).not.toContain('luca.pislaru@gmail.com');
expect(html).not.toContain('mailto:');
await expect(page.locator('.contact-email')).toHaveAttribute('data-enhanced', 'true');
await page.locator('.hero-journey__action--primary').click();
await expect(page).toHaveURL(/#contact$/);
await expect(page.locator('#contact')).toBeInViewport();
await expect(page.locator('.contact-avatar')).toBeVisible();
await expect(page.locator('.contact-social-link')).toHaveText(['LinkedIn', 'GitHub', 'GitLab']);
const missing = await page.locator('[data-testid="toolkit-technology"]').evaluateAll(items => items.filter(item => !item.querySelector('svg')).map(item => item.textContent));
expect(missing).toEqual([]);
for (const name of ['SPSS', 'SoccerNet', 'Microsoft Excel', 'Btrfs', 'rsync']) {
await expect(page.locator('#toolkit')).not.toContainText(name);
}
});
test('email is revealed on focus, with an explicit disclosure and copy fallback', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(`${distUrl}/#contact`);
const email = page.getByRole('link', { name: 'Email Me', exact: true });
await expect(email).toHaveAttribute('href', '#contact-email-details');
await expect(page.locator('.contact-email__preview')).not.toBeVisible();
await email.focus();
await expect(email).toHaveAttribute('href', 'mailto:luca.pislaru@gmail.com');
await page.locator('#contact summary').click();
await expect(page.locator('.contact-email__address')).toHaveText('luca.pislaru@gmail.com');
await page.getByRole('button', { name: 'Copy email' }).click();
await expect(page.locator('.contact-email__status')).toHaveText(/Copied|Select the address to copy it\./);
await page.keyboard.press('Escape');
await expect(page.locator('#contact summary')).toBeFocused();
await expect(page.locator('#contact-email-details')).not.toHaveAttribute('open');
});
test('contact fits desktop and stays aligned and usable across narrow widths', async ({ page, distUrl }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium', 'Viewport matrix is run once.');
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(`${distUrl}/#contact`);
for (const [width, height] of [[1440, 900], [1280, 720], [768, 1024], [390, 844], [320, 740], [844, 390]]) {
await page.setViewportSize({ width: width!, height: height! });
await page.locator('#contact').scrollIntoViewIfNeeded();
const geometry = await page.locator('#contact').evaluate(section => {
const box = section.getBoundingClientRect();
const avatar = section.querySelector('.contact-avatar')!.getBoundingClientRect();
return {
height: box.height,
overflow: document.documentElement.scrollWidth > innerWidth,
socialY: [...section.querySelectorAll('.contact-social-link')].map(el => el.getBoundingClientRect().y),
avatarInside: avatar.right <= box.right && avatar.bottom < section.querySelector('.contact-destination__copy')!.getBoundingClientRect().top,
};
});
expect(geometry.overflow, `${width}px overflow`).toBe(false);
expect(Math.max(...geometry.socialY) - Math.min(...geometry.socialY), `${width}px social alignment`).toBeLessThan(1);
expect(geometry.avatarInside, `${width}px avatar`).toBe(true);
if (width! >= 1280) expect(geometry.height).toBeLessThan(height! - 140);
await page.screenshot({ path: testInfo.outputPath(`contact-${width}x${height}.png`) });
}
});
test('native email disclosure remains usable without JavaScript', async ({ browser, distUrl }) => {
const context = await browser.newContext({ javaScriptEnabled: false, viewport: { width: 390, height: 844 } });
const page = await context.newPage();
await page.goto(`${distUrl}/#contact`);
await expect(page.locator('.contact-email__address')).not.toBeVisible();
await page.locator('#contact summary').click();
await expect(page.locator('.contact-email__address')).toHaveText('luca.pislaru [at] gmail [dot] com');
await expect(page.locator('.video-fallback')).toHaveAttribute('href', '/assets/project-private-game-loop.mp4');
await context.close();
});
test('reduced motion keeps the gameplay still until Play and supports pause', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl);
const figure = page.locator('.project-media--video');
await figure.scrollIntoViewIfNeeded();
await expect(figure.locator('source')).toHaveCount(0);
await figure.getByRole('button', { name: 'Play video' }).click();
await expect(figure.getByRole('button', { name: 'Pause video' })).toBeVisible();
expect(await figure.locator('video').evaluate(element => { const video = element as HTMLVideoElement; return { muted: video.muted, loop: video.loop, paused: video.paused }; })).toEqual({ muted: true, loop: true, paused: false });
await figure.getByRole('button', { name: 'Pause video' }).click();
await expect(figure.getByRole('button', { name: 'Play video' })).toBeVisible();
await expect(figure.locator('source')).toHaveCount(0);
});
test('contact and supporting media have no serious accessibility violations', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(`${distUrl}/#contact`);
await page.locator('#contact summary').click();
const result = await new AxeBuilder({ page }).include('#contact').include('#additional-work').analyze();
expect(result.violations.filter(violation => ['serious', 'critical'].includes(violation.impact ?? ''))).toEqual([]);
});

View File

@@ -226,10 +226,10 @@ test('an aborted generated client entry leaves the complete SSR portfolio usable
await expect(page.locator('main > section')).toHaveCount(8);
await expect(page.locator('nav a')).toHaveCount(8);
await expect(
page.locator('a[href="mailto:luca.pislaru@gmail.com"]'),
page.locator('#contact summary'),
).toBeVisible();
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 a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
});

View File

@@ -55,7 +55,7 @@ test("tabs from the skip link through navigation and every contact action", asyn
const expectedTargets = [
{ selector: ".skip-link", href: "#portfolio-content" },
...navHrefs.map((href) => ({ selector: `nav a[href="${href}"]`, href })),
{ selector: '.hero-journey__action--primary', href: 'mailto:luca.pislaru@gmail.com' },
{ selector: '.hero-journey__action--primary', href: '#contact' },
{ selector: '.hero-journey__actions a[href="#selected-builds"]', href: "#selected-builds" },
{
selector: 'a[href="https://github.com/LucaPisl/footy-tracker"]',
@@ -66,6 +66,14 @@ test("tabs from the skip link through navigation and every contact action", asyn
'a[href="https://github.com/LucaPisl/opinion-dynamics-simulation"]',
href: "https://github.com/LucaPisl/opinion-dynamics-simulation",
},
{
selector: '.video-controls button',
href: null,
},
{
selector: '.video-fallback',
href: '/assets/project-private-game-loop.mp4',
},
{
selector:
'a[href="https://github.com/LucaPisl/CurrencyConverterAvalonia"]',
@@ -76,9 +84,17 @@ test("tabs from the skip link through navigation and every contact action", asyn
href: "https://github.com/LucaPisl/LinuxModdingGuide",
},
{
selector: '#contact a[href^="mailto:"]',
selector: '#contact :is(a, summary):text-is("Email Me")',
href: "mailto:luca.pislaru@gmail.com",
},
{
selector: '#contact summary',
href: null,
},
{
selector: '#contact a[href="https://linkedin.com/in/luca-pisl"]',
href: "https://linkedin.com/in/luca-pisl",
},
{
selector: '#contact a[href="https://github.com/LucaPisl"]',
href: "https://github.com/LucaPisl",
@@ -87,17 +103,13 @@ test("tabs from the skip link through navigation and every contact action", asyn
selector: '#contact a[href="https://gitlab.com/luca_pisl"]',
href: "https://gitlab.com/luca_pisl",
},
{
selector: '#contact a[href="https://linkedin.com/in/luca-pisl"]',
href: "https://linkedin.com/in/luca-pisl",
},
];
for (const target of expectedTargets) {
await page.keyboard.press("Tab");
const locator = page.locator(target.selector);
await expect(locator).toBeFocused();
await expect(locator).toHaveAttribute("href", target.href);
if (target.href !== null) await expect(locator).toHaveAttribute("href", target.href);
await assertVisibleUnobscuredFocus(page);
}
});

View File

@@ -31,7 +31,7 @@ test('F7 replaces a failed poster with exactly one CSS fallback and keeps its de
expect(await readUnhandledRejections(page)).toEqual([]);
});
test('F8 remains authenticated and request-free while the public manifest has no approved video', async ({ page, distUrl }) => {
test('F8 remains authenticated and request-free before the gameplay video is requested', async ({ page, distUrl }) => {
const mediaRequests: string[] = [];
page.on('request', (request) => {
if (/\.(?:mp4|webm)(?:$|[?#])/iu.test(request.url())) mediaRequests.push(request.url());
@@ -40,7 +40,8 @@ test('F8 remains authenticated and request-free while the public manifest has no
await page.goto(authorizedFaultUrl(distUrl, 'F8-video-failure'), { waitUntil: 'networkidle' });
expect((await readFaultSnapshot(page)).fault).toBe('F8-video-failure');
await expect(page.locator('video, video source')).toHaveCount(0);
await expect(page.locator('video')).toHaveCount(1);
await expect(page.locator('video source')).toHaveCount(0);
expect(mediaRequests).toEqual([]);
await assertSemanticFaultOracle(page);
expect(errors.pageErrors).toEqual([]);

View File

@@ -31,7 +31,7 @@ test('approved still evidence uses manifest assets and intrinsic lazy image sema
await expect(page.locator('main > section')).toHaveCount(8);
await expect(page.locator('#selected-builds article')).toHaveCount(3);
await expect(page.locator('#additional-work article')).toHaveCount(4);
await expect(page.locator('[data-project-media]')).toHaveCount(approvedStillCount);
await expect(page.locator('[data-project-media]')).toHaveCount(projectPresentations.length);
await expect(page.locator('[data-project-media] img')).toHaveCount(expectedImages.length);
expect(await page.locator('[data-project-media] img').evaluateAll((images) => images.map((image) => ({
src: new URL((image as HTMLImageElement).src).pathname,
@@ -41,8 +41,9 @@ test('approved still evidence uses manifest assets and intrinsic lazy image sema
loading: (image as HTMLImageElement).loading,
decoding: (image as HTMLImageElement).decoding,
})))).toEqual(expectedImages.map((image) => ({ ...image, loading: 'lazy', decoding: 'async' })));
await expect(page.locator('video, video source')).toHaveCount(0);
await expect(page.locator('[data-project-id="private-medication-tracker"] a')).toHaveCount(0);
await expect(page.locator('video')).toHaveCount(1);
await expect(page.locator('video source')).toHaveCount(0);
await expect(page.locator('[data-project-id="private-medication-tracker"] a.project-link')).toHaveCount(0);
await expect(page.locator('.journey-poster')).not.toHaveAttribute('src', /.+/u);
expect(videoRequests).toEqual([]);
});

View File

@@ -37,10 +37,10 @@ const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> =
};
const visibleTextLeafSelector =
"h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a,svg text";
"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)",
".portfolio-main :is(h1,h2,h3,h4,p,dt,dd,li,figcaption,caption,th,td,a,summary)",
".portfolio-main svg text",
].join(",");
@@ -111,6 +111,7 @@ const expectedVisibleTextSegments = (
(project.media ?? []).flatMap((presentation) => [
presentation.caption,
presentation.longDescription,
...(presentation.kind === "video" ? ["Open gameplay video"] : []),
...(presentation.kind === "chart"
? [
"Variables",
@@ -136,7 +137,7 @@ const expectedVisibleTextSegments = (
content.identity.role,
content.identity.valueProposition,
content.identity.availability,
"Email Luca",
"Contact Me",
"Explore work",
...section("profile", [
content.profile.introduction,
@@ -197,7 +198,6 @@ const expectedVisibleTextSegments = (
project.summary,
...projectMediaText(project),
project.contribution,
project.outcome,
...projectLinkText(project),
]),
),
@@ -212,8 +212,7 @@ const expectedVisibleTextSegments = (
"contact",
[
content.contact.introduction,
"Email Luca",
content.contact.email,
"Email Me",
content.contact.cvMessage,
...content.contact.social.map((link) => link.service),
],
@@ -230,6 +229,7 @@ const rawVisibleTextSegments = async (
({ 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;
@@ -420,12 +420,14 @@ test("keeps every raw SSR reading-path segment visible without JavaScript", asyn
).toBeVisible();
}
const email = page.locator('a[href="mailto:luca.pislaru@gmail.com"]');
await expect(email).toHaveCount(2);
await expect(email).toHaveText(["Email Luca", "Email Luca"]);
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(
"For a current CV or role-specific information, contact me directly.",
"Get in touch for my CV or to talk about a role.",
{ exact: true },
),
).toBeVisible();
@@ -528,5 +530,5 @@ test("retains native section navigation when scripts are disabled", async ({
await expect(page).toHaveURL(`${distUrl}/#contact`);
await expect(page.locator("#contact")).toBeInViewport();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
});

View File

@@ -17,7 +17,7 @@ test('boots reduced motion without requesting the live scene or automatic media'
await expect(page.locator('.journey-canvas')).toHaveCount(0);
await expect(page.locator('.journey-hud')).toBeVisible();
await expect(page.locator('nav[aria-label="Portfolio sections"]')).toBeVisible();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
await expect(page.getByRole('status')).toContainText('reduced motion requested');
expect(requests.some((url) => environmentChunkPattern.test(url))).toBe(false);
await expect(page.locator('video[src], video source[src]')).toHaveCount(0);

View File

@@ -151,7 +151,7 @@ test('renders the validated eight-section portfolio structure before responsive
.toEqual(['#start', '#profile', '#experience', '#infrastructure', '#selected-builds', '#additional-work', '#toolkit', '#contact']);
await expect(page.locator('#selected-builds article, #additional-work article')).not.toHaveCount(0);
await expect(page.locator('#toolkit [data-testid="toolkit-technology"]')).not.toHaveCount(0);
await expect(page.locator('#contact a[href^="mailto:"]')).toHaveCount(1);
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toHaveCount(1);
await expect(page.locator('#contact a[target="_blank"]')).not.toHaveCount(0);
});

View File

@@ -100,11 +100,10 @@ test('exposes the complete semantic portfolio from the generated document', asyn
);
await expect(page.locator('.route-label')).toHaveCount(0);
await expect(page.getByRole('link', { name: 'Explore work' })).toHaveAttribute('href', '#selected-builds');
await expect(page.getByRole('link', { name: 'Email Luca' })).toHaveCount(2);
await expect(page.getByRole('link', { name: 'Contact Me' })).toHaveAttribute('href', '#contact');
await expect(page.getByRole('link', { name: 'Email Me' })).toHaveCount(1);
await expect(page.locator('[data-featured-operation]')).toContainText('without data loss');
expect(await page.locator('[data-testid="toolkit-technology"]', { hasText: 'DNS' }).locator('span').evaluate(
(label) => getComputedStyle(label).gridColumn,
)).toBe('1 / -1');
await expect(page.locator('[data-testid="toolkit-technology"]', { hasText: 'DNS' }).locator('svg')).toHaveCount(1);
expect(await page.locator('.hero-journey').evaluate((hero) => {
const content = hero.querySelector('.hero-journey__content')!;
const heroStyle = getComputedStyle(hero);
@@ -172,8 +171,10 @@ test('keeps canonical native anchors and safe external destinations', async ({
}
const email = page.locator('a[href^="mailto:"]');
await expect(email).toHaveCount(2);
await expect(email).toHaveText(['Email Luca', 'Email Luca']);
await expect(email).toHaveCount(0);
await page.getByRole('link', { name: 'Email Me' }).focus();
await expect(email).toHaveCount(1);
await expect(email).toHaveText('Email Me');
expect(await email.evaluateAll((links) => [...new Set(links.map((link) => link.getAttribute('href')))])).toEqual(
['mailto:luca.pislaru@gmail.com'],
);

View File

@@ -25,7 +25,7 @@ test("falls back to one poster and no scene loop when the deferred scene chunk i
await expect(page.locator("main > section")).toHaveCount(8);
await revealNavigationLinks(page);
await expect(page.locator('nav a[href="#contact"]')).toBeVisible();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
await expect
.poll(
async () => (await readSceneInstrumentation(page)).activeAnimationFrames,
@@ -74,7 +74,7 @@ test("contains a WebGL initialization failure without hiding the portfolio", asy
await expect(page.locator(".journey-poster")).toHaveCount(1);
await expect(page.locator(".journey-canvas")).toHaveCount(0);
await expect(page.locator("#portfolio-content")).toBeVisible();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
await expect
.poll(
async () => (await readSceneInstrumentation(page)).activeAnimationFrames,

View File

@@ -466,7 +466,7 @@ test.describe("production visual composition", () => {
});
await page.locator('nav a[href="#contact"]').click();
await expect(page.locator("#contact-heading")).toBeFocused();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
await expect(page.locator('#contact a[target="_blank"]')).toHaveCount(0);
await captureViewport(page, testInfo, "visual-missing-social");
});
@@ -482,7 +482,7 @@ test.describe("production visual composition", () => {
await expect(page.locator(".journey-hud__section")).toHaveText("Contact");
await expect(page.locator(".journey-hud__arrival")).toHaveText("ARRIVED");
await expect(page.locator("#contact")).toBeInViewport();
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
await expect(page.locator('#contact :is(a, summary):text-is("Email Me")')).toBeVisible();
await captureViewport(page, testInfo, "visual-contact-arrival");
});
});