Files
Portfolio/tests/browser/title-navigation.spec.ts

156 lines
8.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from '../helpers/serve-dist';
test('desktop navigation docks without stretching labels or shifting the document', async ({ page, distUrl }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium');
await page.setViewportSize({ width: 1920, height: 1080 });
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl, { waitUntil: 'networkidle' });
await page.evaluate(() => document.fonts.ready);
const nav = page.locator('.site-navigation');
await expect(nav).toHaveAttribute('data-enhanced', 'true');
// Since round 5 the bar is a full-bleed overhead gantry: it is docked at
// the top and unscaled from the first frame instead of growing into place.
const surfaceScale = () => nav.evaluate(el => new DOMMatrixReadOnly(getComputedStyle(el, '::before').transform).a);
expect(await surfaceScale()).toBe(1);
expect((await nav.boundingBox())!.y).toBeLessThan(1);
const label = nav.locator('a').first();
const initialFont = await label.evaluate(el => getComputedStyle(el).fontSize);
// Measure the content beside this fixed header, not unrelated lazy media
// farther down the page that can settle independently of navigation.
const heroGeometry = () => page.locator('#start').evaluate(el => ({
top: el.getBoundingClientRect().top + scrollY,
height: el.getBoundingClientRect().height,
}));
const initialHero = await heroGeometry();
await page.evaluate(() => scrollTo(0, 100));
await expect.poll(async () => (await nav.boundingBox())!.y).toBeLessThan(1);
await expect.poll(async () => (await nav.boundingBox())!.width).toBe(1920);
await expect.poll(surfaceScale).toBe(1);
expect(await label.evaluate(el => getComputedStyle(el).fontSize)).toBe(initialFont);
expect(await heroGeometry()).toEqual(initialHero);
await page.evaluate(() => scrollTo(0, 0));
await expect.poll(async () => (await nav.boundingBox())!.y).toBeLessThan(1);
});
test('selected navigation has rounded inset highlights', async ({ page, distUrl }) => {
await page.goto(distUrl);
const disclosure = page.locator('.site-navigation summary');
if (await disclosure.isVisible()) await disclosure.click();
const selected = page.locator('.site-navigation a[aria-current="page"]');
// On the desktop gantry the current exit is marked by its amber lane lamp
// (the li::after rule) rather than a rounded inset; the menu keeps the inset.
const marker = await selected.evaluate(el => {
const lamp = getComputedStyle(el.parentElement!, '::after');
return {
radius: parseFloat(getComputedStyle(el).borderTopLeftRadius),
lamp: lamp.content !== 'none' && parseFloat(lamp.blockSize) >= 2,
};
});
expect(marker.radius >= 5 || marker.lamp).toBe(true);
await selected.focus();
await page.keyboard.press('Shift');
expect(await selected.evaluate(el => getComputedStyle(el).outlineStyle)).not.toBe('none');
});
test('skip link uses a localized visible cue and Tab continues into content', async ({ page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl);
await page.keyboard.press('Tab');
await expect(page.locator('.skip-link')).toBeFocused();
await page.keyboard.press('Enter');
const main = page.locator('#portfolio-content');
await expect(main).toBeFocused();
expect(await main.evaluate(el => getComputedStyle(el).boxShadow)).toBe('none');
expect(await main.evaluate(el => getComputedStyle(el).outlineStyle)).toBe('none');
await expect(page.locator('.content-focus-cue')).toBeVisible();
const cue = await page.locator('.content-focus-cue').boundingBox();
expect(cue!.width).toBeLessThan(240);
expect(cue!.height).toBeLessThan(60);
await page.keyboard.press('Tab');
await expect(page.locator('.hero-journey__action--primary')).toBeFocused();
expect(await page.locator('.hero-journey__action--primary').evaluate(el => getComputedStyle(el).outlineStyle)).not.toBe('none');
await expect(page.locator('.content-focus-cue')).not.toBeVisible();
});
test('Shift cannot draw a page-wide focus frame in the live reading stage', async ({ page, distUrl }) => {
await page.goto(distUrl);
await page.locator('main').evaluate(el => (el as HTMLElement).focus({ preventScroll: true }));
await page.keyboard.press('Shift');
expect(await page.locator('main').evaluate(el => ({ outline: getComputedStyle(el).outlineStyle, shadow: getComputedStyle(el).boxShadow }))).toEqual({ outline: 'none', shadow: 'none' });
await page.locator('.hero-journey__action--primary').focus();
await page.keyboard.press('Shift');
expect(await page.locator('.hero-journey__action--primary').evaluate(el => getComputedStyle(el).outlineStyle)).not.toBe('none');
});
test('name decoration pauses offscreen and leaves one readable heading', async ({ page, distUrl }) => {
await page.goto(distUrl);
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Luca Pîslaru');
await page.evaluate(() => document.fonts.ready);
const title = await page.locator('.hero-name').evaluate(el => ({
height: el.getBoundingClientRect().height,
lineHeight: parseFloat(getComputedStyle(el).lineHeight),
}));
expect(title.height).toBeLessThanOrEqual(title.lineHeight * 2.1);
const particles = page.locator('.hero-name-particles');
await expect(particles).toHaveAttribute('aria-hidden', 'true');
await expect(particles).toHaveAttribute('data-active', 'true');
const canvas = particles.locator('canvas');
await expect(canvas).toHaveCount(1);
const nameBox = await particles.boundingBox();
const effectBox = await canvas.boundingBox();
expect(effectBox!.x).toBeCloseTo(nameBox!.x - 12, 0);
expect(effectBox!.width).toBeCloseTo(nameBox!.width + 24, 0);
const roleBox = await page.locator('.hero-journey__role').boundingBox();
// The complete drawing area (including every wave crest) clears the role.
expect(roleBox!.y - (effectBox!.y + effectBox!.height)).toBeGreaterThanOrEqual(4);
await expect.poll(() => canvas.evaluate(el => {
const context = (el as HTMLCanvasElement).getContext('2d')!;
const { width, height } = el as HTMLCanvasElement;
// Sparks occupy the name area, while the waves sit below it.
return [
context.getImageData(0, 0, width, Math.floor(height * 0.75)).data.some(value => value > 0),
context.getImageData(0, Math.floor(height * 0.92), width, Math.max(1, Math.floor(height * 0.08))).data.some(value => value > 0),
];
})).toEqual([true, true]);
expect(await particles.evaluate(el => getComputedStyle(el, '::after').animationName)).toBe('name-sheen');
expect(await particles.evaluate(el => getComputedStyle(el, '::after').animationPlayState)).toBe('running');
const firstFrame = await canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL());
await expect.poll(() => canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL())).not.toBe(firstFrame);
await page.locator('#contact').scrollIntoViewIfNeeded();
await expect(particles).toHaveAttribute('data-active', 'false');
expect(await particles.evaluate(el => getComputedStyle(el, '::after').animationPlayState)).toBe('paused');
const stoppedFrame = await canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL());
await page.waitForTimeout(150);
expect(await canvas.evaluate(el => (el as HTMLCanvasElement).toDataURL())).toBe(stoppedFrame);
});
test('wave animation clears the role text across desktop, short screens, and narrow phones', async ({ page, distUrl }, testInfo) => {
test.skip(testInfo.project.name !== 'desktop-chromium');
await page.goto(distUrl);
for (const [width, height] of [[1920, 1080], [1024, 768], [820, 600], [768, 520], [393, 851], [320, 640]]) {
await page.setViewportSize({ width: width!, height: height! });
await page.evaluate(() => document.fonts.ready);
await expect.poll(async () => {
const effect = await page.locator('.hero-name-particles canvas').boundingBox();
const role = await page.locator('.hero-journey__role').boundingBox();
return role!.y - (effect!.y + effect!.height);
}, { message: `Wave clearance at ${width} × ${height}` }).toBeGreaterThanOrEqual(4);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
}
});
test('reduced motion and no-JavaScript retain the name and native skip link', async ({ browser, page, distUrl }) => {
await page.emulateMedia({ reducedMotion: 'reduce' });
await page.goto(distUrl);
await expect(page.locator('.hero-name-particles')).toHaveAttribute('data-active', 'false');
const context = await browser.newContext({ javaScriptEnabled: false });
const native = await context.newPage();
await native.goto(distUrl);
await expect(native.getByRole('heading', { level: 1 })).toHaveText('Luca Pîslaru');
await native.keyboard.press('Tab');
await native.keyboard.press('Enter');
await expect(native.locator('main')).toBeFocused();
await expect(native.locator('.content-focus-cue')).toBeVisible();
await context.close();
});