142 lines
4.6 KiB
TypeScript
142 lines
4.6 KiB
TypeScript
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();
|
|
});
|