239 lines
7.3 KiB
TypeScript
239 lines
7.3 KiB
TypeScript
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;
|
|
|
|
const visibleTextLeafSelector = 'h1,h2,h3,p,dt,dd,li,figcaption,a';
|
|
const visibleTextSelector = [
|
|
`.site-header :is(${visibleTextLeafSelector})`,
|
|
`.portfolio-main :is(${visibleTextLeafSelector})`,
|
|
].join(',');
|
|
|
|
const normalizedText = (value: string): string =>
|
|
value.replace(/\s+/gu, ' ').trim();
|
|
|
|
const rawVisibleTextSegments = async (
|
|
page: import('@playwright/test').Page,
|
|
rawHtml: string,
|
|
): Promise<string[]> =>
|
|
page.evaluate(
|
|
({ html, leafSelector, rootSelector }) => {
|
|
const document = new DOMParser().parseFromString(html, 'text/html');
|
|
return [...document.querySelectorAll<HTMLElement>(rootSelector)]
|
|
.filter(
|
|
(element) =>
|
|
element.closest('[aria-hidden="true"]') === null &&
|
|
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<string[]> =>
|
|
page.locator(visibleTextSelector).evaluateAll(
|
|
(elements, leafSelector) =>
|
|
elements
|
|
.filter((element) => {
|
|
const htmlElement = element as HTMLElement;
|
|
const style = getComputedStyle(htmlElement);
|
|
return (
|
|
htmlElement.closest('[aria-hidden="true"]') === null &&
|
|
htmlElement.querySelector(leafSelector) === null &&
|
|
htmlElement.getClientRects().length > 0 &&
|
|
style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
style.opacity !== '0'
|
|
);
|
|
})
|
|
.map((element) =>
|
|
(element as HTMLElement).innerText.replace(/\s+/gu, ' ').trim(),
|
|
)
|
|
.filter((text) => text.length > 0),
|
|
visibleTextLeafSelector,
|
|
);
|
|
|
|
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 topElement = document.elementFromPoint(
|
|
left + (right - left) / 2,
|
|
top + Math.min(bottom - top, window.innerHeight) / 2,
|
|
);
|
|
const style = getComputedStyle(active);
|
|
return (
|
|
topElement !== null &&
|
|
(active === topElement || active.contains(topElement)) &&
|
|
style.outlineStyle !== 'none' &&
|
|
Number.parseFloat(style.outlineWidth) >= 2
|
|
);
|
|
}),
|
|
)
|
|
.toBe(true);
|
|
};
|
|
|
|
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 expectedVisibleText = await rawVisibleTextSegments(page, rawHtml);
|
|
|
|
await page.goto(distUrl, { waitUntil: 'load' });
|
|
const actualVisibleText = await browserVisibleTextSegments(page);
|
|
|
|
expect(expectedVisibleText.length).toBeGreaterThan(80);
|
|
expect(actualVisibleText).toEqual(expectedVisibleText);
|
|
expect(normalizedText(actualVisibleText.join(' '))).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('.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('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 assertVisibleUnobscuredFocus(page);
|
|
});
|
|
|
|
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();
|
|
});
|