Files
Lucas WintherandClaude Opus 5.5 fdad3e86a0 feat(site): the start page's gate is a plain page script, no React
Gate.tsx (an island that rendered nothing) becomes scripts/gate.ts with its key
handling and readout text as pure functions. With the switch already plain, the
start page has no island and loads none of the React runtime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 09:49:38 +01:00

84 lines
3.6 KiB
TypeScript

import { expect, test } from '@playwright/test';
import { blockThirdParty } from './helpers.ts';
// The gate's behaviour is a plain module script (spec 006); module scripts have run by the time page.goto
// resolves on `load`, so there is no island to wait for.
test.beforeEach(async ({ page }) => {
await blockThirdParty(page);
});
test('clicking each side navigates', async ({ page }) => {
await page.goto('/');
await page.locator('a[data-enter="work"]').click();
await expect(page).toHaveURL(/\/work\/$/);
await page.goto('/');
await page.locator('a[data-enter="personal"]').click();
await expect(page).toHaveURL(/\/off-the-clock\/$/);
});
test('the everything pill is hidden for now, and G does nothing', async ({ page, isMobile }) => {
await page.goto('/');
await expect(page.locator('#garden-link')).toHaveCount(0);
test.skip(!!isMobile, 'keyboard shortcut');
await page.keyboard.press('g');
await page.waitForTimeout(300);
await expect(page).toHaveURL(/\/$/);
});
test('arrows lean and Enter enters', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'keyboard shortcut');
await page.goto('/');
await expect(async () => {
await page.keyboard.press('ArrowRight');
await expect(page.locator('#gate')).toHaveClass(/lean-art/, { timeout: 500 });
}).toPass();
await page.keyboard.press('ArrowLeft');
await expect(page.locator('#gate')).toHaveClass(/lean-tech/);
await page.keyboard.press('Enter');
await expect(page).toHaveURL(/\/work\/$/);
});
test('nothing animates on load', async ({ page }) => {
// A pointer resting over a side would legitimately lean it; take the page out of hit testing
// from the first moment so only the page's own load-time motion is measured.
await page.addInitScript(() => {
new MutationObserver((_m, obs) => {
if (!document.documentElement) return;
const st = document.createElement('style');
st.textContent = '*{pointer-events:none!important}';
document.documentElement.append(st);
obs.disconnect();
}).observe(document, { childList: true });
});
await page.goto('/');
await page.waitForLoadState('load');
const running = await page.evaluate(() =>
document.getAnimations().map((a) => `${(a.effect as KeyframeEffect | null)?.target?.tagName ?? '?'} ${(a as CSSAnimation).animationName ?? (a as CSSTransition).transitionProperty}`),
);
expect(running).toEqual([]);
});
test('the gate fills the screen on tall and large displays', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'phones stack the halves and scroll');
for (const [width, height] of [[1280, 720], [1920, 1080], [2560, 1440], [1440, 1600]]) {
await page.setViewportSize({ width, height });
await page.goto('/');
const m = await page.evaluate(() => ({
gate: document.querySelector('#gate')!.getBoundingClientRect().height,
foot: document.querySelector('.foot')!.getBoundingClientRect().bottom,
doc: document.documentElement.scrollHeight,
}));
expect(Math.round(m.foot), `${width}x${height}`).toBe(height); // footer sits on the bottom edge
expect(m.doc, `${width}x${height}`).toBe(height); // and nothing scrolls
// Scaled up or not, the "everything" pill (when shown) stays on the seam between the halves.
if (await page.locator('.garden-link').count()) await expect(async () => {
const seam = await page.evaluate(() => {
const r = document.querySelector('.garden-link')!.getBoundingClientRect();
return r.left + r.width / 2 - document.querySelector('#tech')!.getBoundingClientRect().right;
});
expect(Math.abs(seam)).toBeLessThan(2);
}).toPass({ timeout: 3000 });
}
});