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'); const surfaceScale = () => nav.evaluate(el => new DOMMatrixReadOnly(getComputedStyle(el, '::before').transform).a); expect(await surfaceScale()).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).toBeGreaterThan(10); }); 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"]'); expect(await selected.evaluate(el => parseFloat(getComputedStyle(el).borderTopLeftRadius))).toBeGreaterThanOrEqual(5); 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(); });