Files
portfolio-next/e2e/work.spec.ts
T
Lucas WintherandClaude Opus 5.5 974b3eb0df feat(work): CV-first work page, six proud projects, record and courses (spec 009)
- CV header with highlights and keeping-up summary replaces the intro and Now strip
- "Projects I'm proudest of": up to six featured panels; screenshots muted until hover
- "Also made" list; searching it also lists matching courses
- Record section: recognition & volunteering, courses by topic
- /work/learning/: searchable course list (plain script), in the sitemap
- Top bar: Projects, Record, Writing, CV; no Bluesky link in the work footer
- Highlights: only the placement takes the accent (owner, after the preview)

Owner looked at the rendered preview on 2026-11-12 before this commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 10:44:46 +01:00

212 lines
9.5 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { blockThirdParty, openWork, setPref, P } from './helpers.ts';
test.beforeEach(async ({ page }) => {
await blockThirdParty(page);
});
const row = (page: Page, id: string) => page.locator(`.idx a.row[data-open-sheet="${id}"]`);
const sheet = (page: Page) => page.getByRole('dialog');
test('opening a project pushes its URL; Esc restores /work/', async ({ page }) => {
await openWork(page);
await row(page, P.byHand2).click();
await expect(page).toHaveURL(new RegExp(`/work/${P.byHand2}/$`));
await expect(sheet(page)).toBeVisible();
await page.keyboard.press('Escape');
await expect(sheet(page)).toBeHidden();
await expect(page).toHaveURL(/\/work\/$/);
});
test('sheet history: Back closes, Forward reopens with focus inside', async ({ page }) => {
await openWork(page);
await row(page, P.byHand2).click();
await expect(sheet(page)).toBeVisible();
await page.goBack();
await expect(sheet(page)).toBeHidden();
await expect(page).toHaveURL(/\/work\/$/);
await page.goForward();
await expect(page).toHaveURL(new RegExp(`/work/${P.byHand2}/$`));
await expect(sheet(page)).toBeVisible();
await expect.poll(() => page.evaluate(() => !!document.activeElement?.closest('[role="dialog"]'))).toBe(true);
});
test('focus moves into the sheet, is trapped, and returns to the row', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'keyboard focus');
await openWork(page);
const opener = row(page, P.byHand2);
await opener.focus();
await page.keyboard.press('Enter');
await expect(sheet(page)).toBeVisible();
await expect.poll(() => page.evaluate(() => !!document.activeElement?.closest('[role="dialog"]'))).toBe(true);
for (let i = 0; i < 25; i++) {
await page.keyboard.press('Tab');
expect(await page.evaluate(() => !!document.activeElement?.closest('[role="dialog"]'))).toBe(true);
}
for (let i = 0; i < 25; i++) {
await page.keyboard.press('Shift+Tab');
expect(await page.evaluate(() => !!document.activeElement?.closest('[role="dialog"]'))).toBe(true);
}
await page.keyboard.press('Escape');
await expect(sheet(page)).toBeHidden();
await expect(opener).toBeFocused();
});
async function visibleRowIds(page: Page): Promise<string[]> {
return page.$$eval('.idx a.row', (as) => as.filter((a) => a.getClientRects().length > 0).map((a) => (a as HTMLElement).dataset.openSheet!));
}
test('arrow keys step through the visible rows', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'keyboard');
await openWork(page);
const ids = await visibleRowIds(page);
expect(ids.length).toBeGreaterThan(2);
await row(page, ids[0]!).click();
await expect(page).toHaveURL(new RegExp(`/work/${ids[0]}/$`));
await page.keyboard.press('ArrowRight');
await expect(page).toHaveURL(new RegExp(`/work/${ids[1]}/$`));
await page.keyboard.press('ArrowLeft');
await expect(page).toHaveURL(new RegExp(`/work/${ids[0]}/$`));
// One history entry only: a single Back closes the sheet.
await page.goBack();
await expect(page).toHaveURL(/\/work\/$/);
await expect(sheet(page)).toBeHidden();
});
test('arrows step through featured panels from a featured link', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'keyboard');
await openWork(page);
await page.locator(`article.feat[data-id="${P.byHand}"] h3 a[data-open-sheet]`).click();
await expect(page).toHaveURL(new RegExp(`/work/${P.byHand}/$`));
await page.keyboard.press('ArrowRight');
await expect(page).toHaveURL(new RegExp(`/work/${P.someAi}/$`));
});
test.describe('under an active filter', () => {
// List order (newest first): garden-bot (ai-made), placeholder-notes (unknown), course-planner (by-hand).
test('by hand only: arrows never reach hidden rows', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'keyboard');
await setPref(page, 'by-hand-only');
await openWork(page);
expect(await visibleRowIds(page)).toEqual([P.byHand2]);
await row(page, P.byHand2).click();
await page.keyboard.press('ArrowLeft');
await page.keyboard.press('ArrowRight');
await expect(page).toHaveURL(new RegExp(`/work/${P.byHand2}/$`));
});
test('not fully by AI: arrows skip the ai-made row', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'keyboard');
await setPref(page, 'not-fully-ai');
await openWork(page);
expect(await visibleRowIds(page)).toEqual([P.unknown, P.byHand2]);
await row(page, P.unknown).click();
await page.keyboard.press('ArrowLeft'); // the ai-made row above is hidden
await expect(page).toHaveURL(new RegExp(`/work/${P.unknown}/$`));
await page.keyboard.press('ArrowRight');
await expect(page).toHaveURL(new RegExp(`/work/${P.byHand2}/$`));
});
});
test.describe('under by-hand-only', () => {
test.beforeEach(async ({ page }) => setPref(page, 'by-hand-only'));
test('a hidden featured panel shows a note; show anyway reveals it', async ({ page }) => {
await openWork(page);
const panel = page.locator(`article.feat[data-id="${P.someAi}"]`);
const note = panel.locator('xpath=following-sibling::*[1]');
await expect(panel).toBeHidden();
await expect(note).toBeVisible();
await expect(note).toContainText('Fixture Dashboard');
await note.getByRole('button', { name: 'show anyway' }).click();
await expect(panel).toBeVisible();
await expect(note).toBeHidden();
});
});
test('"/" focuses search', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'keyboard');
await openWork(page);
await page.keyboard.press('/');
await expect(page.getByRole('searchbox', { name: /Search projects/ })).toBeFocused();
});
test('"/" focuses the search right after load, without scrolling to the list', async ({ page }) => {
await page.goto('/work/');
await page.waitForLoadState('load');
await page.waitForFunction(() => 'requestIdleCallback' in window);
const search = page.getByRole('searchbox', { name: /Search projects/ });
// The list sits below the fold; it must hydrate on idle, not when scrolled into view.
await expect.poll(async () => {
await page.keyboard.press('/');
return search.evaluate((el) => el === document.activeElement);
}, { timeout: 5_000 }).toBe(true);
});
test('searching the list shows matching courses with the count and a link to the learning page', async ({ page }) => {
await openWork(page);
const search = page.getByRole('searchbox', { name: /Search projects/ });
const strip = page.locator('.idx .cstrip');
await expect(strip).toHaveCount(0);
await search.fill('java');
await expect(page.locator('.idx .sh .n')).toHaveText('1 project · 6 courses match');
await expect(strip.locator('.crow')).toHaveCount(5);
// Newest first; the seal marks a certificate, the tag marks a lab.
await expect(strip.locator('.crow').first()).toContainText('Profiling the JVM');
await expect(strip.locator('.crow').first()).toContainText('lab');
await expect(strip.locator('.crow').first().locator('.seal')).toHaveCount(1);
await expect(strip.getByRole('link', { name: '+ 1 more on the learning page →' })).toHaveAttribute('href', '/work/learning/?q=java');
// The AI preference never hides courses.
await search.fill('');
await expect(strip).toHaveCount(0);
});
test('screenshots are muted until hovered; drawings are not', async ({ page, isMobile }) => {
test.skip(!!isMobile, 'hover');
await openWork(page);
const img = page.locator('article.feat[data-id="fixture-photo-board"] .s-image img');
await img.scrollIntoViewIfNeeded();
await expect.poll(() => img.evaluate((el) => getComputedStyle(el).filter)).toContain('saturate(0.5)');
await page.locator('article.feat[data-id="fixture-photo-board"] h3').hover();
await expect.poll(() => img.evaluate((el) => getComputedStyle(el).filter)).toBe('none');
const drawing = page.locator('article.feat[data-id="example-compute-portal"] .stage');
expect(await drawing.evaluate((el) => getComputedStyle(el).filter)).toBe('none');
});
test.describe('/work/learning/', () => {
const rows = (page: Page) => page.locator('tr[data-course]:visible');
test('lists every course; search and the topic buttons hide rows', async ({ page }) => {
await page.goto('/work/learning/');
await expect(rows(page)).toHaveCount(11);
const search = page.getByRole('searchbox', { name: 'Search courses' });
await search.fill('docker');
await expect(rows(page)).toHaveCount(1);
await expect(rows(page).first()).toContainText('Containers from Scratch');
await search.fill('');
await page.getByRole('button', { name: /^Java/ }).click();
await expect(page.getByRole('button', { name: /^Java/ })).toHaveAttribute('aria-pressed', 'true');
await expect(rows(page)).toHaveCount(6);
await search.fill('docker');
await expect(rows(page)).toHaveCount(0);
await expect(page.getByText('No course matches')).toBeVisible();
await page.getByRole('button', { name: /^All/ }).click();
await expect(rows(page)).toHaveCount(1);
await expect(page.getByText('No course matches')).toBeHidden();
});
test('?q= pre-filters the table', async ({ page }) => {
await page.goto('/work/learning/?q=java');
await expect(page.getByRole('searchbox', { name: 'Search courses' })).toHaveValue('java');
await expect(rows(page)).toHaveCount(6);
});
test('the search strip on /work/ leads here with the query', async ({ page }) => {
await openWork(page);
await page.getByRole('searchbox', { name: /Search projects/ }).fill('java');
await page.getByRole('link', { name: /more on the learning page/ }).click();
await expect(page).toHaveURL(/\/work\/learning\/\?q=java$/);
await expect(rows(page)).toHaveCount(6);
});
});