test: lock semantic and no-script portfolio behavior

This commit is contained in:
2026-09-02 03:12:12 +03:00
parent 3638a0beef
commit 091ff70deb
8 changed files with 811 additions and 2 deletions

View File

@@ -0,0 +1,133 @@
import { expect, test } from '../helpers/serve-dist';
interface DocumentSnapshot {
text: string;
sectionIds: string[];
sectionHeadings: string[];
h1: string;
role: string;
landmarks: {
headers: number;
mains: number;
navigations: number;
footers: number;
};
}
const captureDocument = async (
page: import('@playwright/test').Page,
): Promise<DocumentSnapshot> =>
page.evaluate(() => ({
text: (document.querySelector('.portfolio-shell')?.textContent ?? '')
.replace(/\s+/gu, ' ')
.trim(),
sectionIds: [
...document.querySelectorAll<HTMLElement>('main > section'),
].map((section) => section.id),
sectionHeadings: [
...document.querySelectorAll<HTMLElement>('main > section > h2'),
].map((heading) => heading.innerText.trim()),
h1: document.querySelector('h1')?.textContent?.trim() ?? '',
role: document.querySelector('.site-header > p')?.textContent?.trim() ?? '',
landmarks: {
headers: document.querySelectorAll('header').length,
mains: document.querySelectorAll('main').length,
navigations: document.querySelectorAll('nav').length,
footers: document.querySelectorAll('footer').length,
},
}));
test('hydration preserves the built subject matter and landmark structure without errors or external fetches', async ({
browser,
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === 'mobile-chromium',
'Hydration identity is covered once; semantic and no-script behavior run on mobile.',
);
const beforePage = await browser.newPage({
javaScriptEnabled: false,
viewport: { width: 1440, height: 900 },
});
await beforePage.goto(distUrl, { waitUntil: 'load' });
const before = await captureDocument(beforePage);
await beforePage.close();
const pageErrors: string[] = [];
const consoleErrors: string[] = [];
const failedRequests: string[] = [];
const requests: string[] = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') consoleErrors.push(message.text());
});
page.on('request', (request) => requests.push(request.url()));
page.on('requestfailed', (request) =>
failedRequests.push(
`${request.url()}: ${request.failure()?.errorText ?? 'failed'}`,
),
);
await page.addInitScript(() => {
const unhandled: string[] = [];
Reflect.set(globalThis, '__portfolioUnhandledRejections', unhandled);
addEventListener('unhandledrejection', (event) =>
unhandled.push(String(event.reason)),
);
});
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => {
const app = document.querySelector('#app');
return app !== null && Reflect.has(app, '__vue_app__');
});
await page.waitForFunction(() => document.readyState === 'complete');
await page.evaluate(
() =>
new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
),
);
const after = await captureDocument(page);
const unhandledRejections = await page.evaluate(
() =>
(Reflect.get(globalThis, '__portfolioUnhandledRejections') as
string[] | undefined) ?? [],
);
expect(after).toEqual(before);
expect(after.h1).toBe('Luca Pîslaru');
expect(after.role).toBe('System Administrator & Full-Stack Developer');
expect(after.sectionIds).toEqual([
'start',
'profile',
'experience',
'infrastructure',
'selected-builds',
'additional-work',
'toolkit',
'contact',
]);
expect(after.landmarks).toEqual({
headers: 1,
mains: 1,
navigations: 1,
footers: 1,
});
expect(pageErrors).toEqual([]);
expect(consoleErrors).toEqual([]);
expect(unhandledRejections).toEqual([]);
expect(failedRequests).toEqual([]);
expect(requests.length).toBeGreaterThan(1);
expect(
requests.every((requestUrl) => {
const url = new URL(requestUrl);
return (
url.origin === distUrl &&
(url.pathname === '/' || url.pathname.startsWith('/assets/'))
);
}),
).toBe(true);
});

View File

@@ -0,0 +1,141 @@
import { expect, test } from '../helpers/serve-dist';
const navHrefs = [
'#start',
'#profile',
'#experience',
'#infrastructure',
'#selected-builds',
'#additional-work',
'#toolkit',
'#contact',
] as const;
const assertVisibleUnobscuredFocus = async (
page: import('@playwright/test').Page,
): Promise<void> => {
await expect
.poll(() =>
page.evaluate(() => {
const active = document.activeElement;
if (!(active instanceof HTMLElement)) return false;
const rect = active.getBoundingClientRect();
const left = Math.max(0, rect.left);
const right = Math.min(window.innerWidth, rect.right);
const top = Math.max(0, rect.top);
const bottom = Math.min(window.innerHeight, rect.bottom);
if (right <= left || bottom <= top) return false;
const x = left + (right - left) / 2;
const y = top + (bottom - top) / 2;
const topElement = document.elementFromPoint(x, y);
const style = getComputedStyle(active);
return (
topElement !== null &&
(active === topElement ||
active.contains(topElement) ||
topElement.contains(active)) &&
style.outlineStyle !== 'none' &&
Number.parseFloat(style.outlineWidth) >= 2
);
}),
)
.toBe(true);
};
test('tabs from the skip link through navigation and every contact action', async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
const expectedTargets = [
{ selector: '.skip-link', href: '#portfolio-content' },
...navHrefs.map((href) => ({ selector: `nav a[href="${href}"]`, href })),
{
selector: '#contact a[href^="mailto:"]',
href: 'mailto:luca.pislaru@gmail.com',
},
{
selector: '#contact a[href="https://github.com/LucaPisl"]',
href: 'https://github.com/LucaPisl',
},
{
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);
await assertVisibleUnobscuredFocus(page);
}
});
test('explicit section activation scrolls to and focuses its heading', async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === 'mobile-chromium',
'The full keyboard path already covers the mobile project.',
);
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
for (let index = 0; index < 5; index += 1) await page.keyboard.press('Tab');
await expect(page.locator('nav a[href="#infrastructure"]')).toBeFocused();
await page.keyboard.press('Enter');
await expect(page).toHaveURL(`${distUrl}/#infrastructure`);
await expect(page.locator('#infrastructure')).toBeInViewport();
await expect(page.locator('#infrastructure-heading')).toHaveAttribute(
'tabindex',
'-1',
);
await expect(page.locator('#infrastructure-heading')).toBeFocused();
await assertVisibleUnobscuredFocus(page);
});
test('manual wheel and PageDown scrolling never move focus', async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === 'mobile-chromium',
'The focus invariant is engine-level rather than viewport-specific.',
);
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await page.keyboard.press('Tab');
await page.keyboard.press('Tab');
const startLink = page.locator('nav a[href="#start"]');
await expect(startLink).toBeFocused();
await page.mouse.wheel(0, 900);
await expect(startLink).toBeFocused();
await page.keyboard.press('PageDown');
await expect(startLink).toBeFocused();
});
test('direct fragments do not steal focus while reduced-motion navigation remains accessible', async ({
page,
distUrl,
}, testInfo) => {
test.skip(
testInfo.project.name === 'mobile-chromium',
'The full keyboard path already covers the mobile project.',
);
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(`${distUrl}/#toolkit`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('#toolkit-heading')).not.toBeFocused();
await page.locator('nav a[href="#contact"]').click();
await expect(page).toHaveURL(`${distUrl}/#contact`);
await expect(page.locator('#contact-heading')).toBeFocused();
await expect(page.locator('#contact')).toBeInViewport();
});

View File

@@ -0,0 +1,117 @@
import { expect, 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 = [
'START',
'PROFILE',
'EXPERIENCE',
'INFRASTRUCTURE',
'SELECTED BUILDS',
'ADDITIONAL WORK',
'TOOLKIT',
'CONTACT',
] as const;
test('keeps the complete hiring-manager path readable without JavaScript', async ({
page,
distUrl,
}) => {
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'}`,
);
}
});
await page.goto(distUrl, { waitUntil: 'load' });
await expect(page.locator('h1:visible')).toHaveCount(1);
await expect(page.locator('h1')).toHaveText('Luca Pîslaru');
await expect(page.locator('.site-header > p').first()).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();
}
const email = page.locator('a[href="mailto:luca.pislaru@gmail.com"]');
await expect(email).toBeVisible();
await expect(email).toHaveText('luca.pislaru@gmail.com');
await expect(
page.getByText(
'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',
]) {
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('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 a[href^="mailto:"]')).toBeVisible();
});

View File

@@ -0,0 +1,179 @@
import { request as httpRequest } from 'node:http';
import { symlink, unlink } from 'node:fs/promises';
import { resolve } from 'node:path';
import { expect, test } from '../helpers/serve-dist';
const sectionIds = [
'start',
'profile',
'experience',
'infrastructure',
'selected-builds',
'additional-work',
'toolkit',
'contact',
] as const;
const sectionHeadings = [
'START',
'PROFILE',
'EXPERIENCE',
'INFRASTRUCTURE',
'SELECTED BUILDS',
'ADDITIONAL WORK',
'TOOLKIT',
'CONTACT',
] as const;
const rawStatus = async (
origin: string,
path: string,
method = 'GET',
): Promise<number | undefined> =>
new Promise((resolve, reject) => {
const url = new URL(origin);
const request = httpRequest(
{
host: url.hostname,
method,
path,
port: url.port,
},
(response) => {
response.resume();
response.once('end', () => resolve(response.statusCode));
},
);
request.once('error', reject);
request.end();
});
test('serves only the owned generated preview surface', async ({
distUrl,
request,
}, testInfo) => {
const document = await request.get(`${distUrl}/`);
expect(document.status()).toBe(200);
expect(document.headers()['content-type']).toBe('text/html; charset=utf-8');
const unsafeMethod = await request.post(`${distUrl}/index.html`);
expect(unsafeMethod.status()).toBe(405);
expect(unsafeMethod.headers().allow).toBe('GET, HEAD');
expect(await rawStatus(distUrl, '/%2e%2e/package.json')).toBe(403);
expect(
(await request.get(`${distUrl}/not-a-preview-resource`)).status(),
).toBe(404);
const linkName = `.task-7-escape-${process.pid}-${testInfo.workerIndex}`;
const linkPath = resolve(process.cwd(), 'dist', linkName);
await symlink(resolve(process.cwd(), 'package.json'), linkPath);
try {
expect((await request.get(`${distUrl}/${linkName}`)).status()).toBe(403);
} finally {
await unlink(linkPath);
}
});
test('exposes the complete semantic portfolio from the generated document', async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
await expect(page.locator('h1:visible')).toHaveCount(1);
await expect(page.locator('h1')).toHaveText('Luca Pîslaru');
await expect(page.locator('.site-header > p').first()).toHaveText(
'System Administrator & Full-Stack Developer',
);
await expect(page.locator('main')).toHaveCount(1);
await expect(
page.locator('nav[aria-label="Portfolio sections"]'),
).toHaveCount(1);
await expect(page.locator('footer')).toHaveCount(1);
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,
);
expect(
await page.locator('h1, h2, h3').evaluateAll((headings) => {
const levels = headings.map((heading) =>
Number(heading.tagName.slice(1)),
);
return levels.every(
(level, index) => index === 0 || level <= levels[index - 1]! + 1,
);
}),
).toBe(true);
});
test('keeps canonical native anchors and safe external destinations', async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
const navigation = page.locator('nav[aria-label="Portfolio sections"] a');
expect(
await navigation.evaluateAll((links) =>
links.map((link) => link.getAttribute('href')),
),
).toEqual(sectionIds.map((id) => `#${id}`));
for (const id of sectionIds) {
await expect(page.locator(`#${id}`)).toHaveCount(1);
await expect(page.locator(`#${id}-heading`)).toHaveCount(1);
}
const email = page.locator('a[href^="mailto:"]');
await expect(email).toHaveCount(1);
await expect(email).toHaveText('luca.pislaru@gmail.com');
const externalLinks = page.locator('#contact a[target="_blank"]');
await expect(externalLinks).toHaveCount(3);
expect(
await externalLinks.evaluateAll((links) =>
links.every((link) => {
const rel = new Set((link.getAttribute('rel') ?? '').split(/\s+/u));
return (
link instanceof HTMLAnchorElement &&
link.protocol === 'https:' &&
rel.has('noopener') &&
rel.has('noreferrer')
);
}),
),
).toBe(true);
});
test('never links private projects or packages a résumé download', async ({
page,
distUrl,
}) => {
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
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(
await page
.locator('a')
.evaluateAll((links) =>
links.some(
(link) =>
/(?:résumé|resume|\bcv\b)/iu.test(link.textContent ?? '') ||
/\.(?:pdf|docx?)(?:$|[?#])/iu.test(link.getAttribute('href') ?? ''),
),
),
).toBe(false);
});