- 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>
207 lines
11 KiB
TypeScript
207 lines
11 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
import { blockThirdParty, hydrated, openWork, setPref, P, I, PREF_KEY } from './helpers.ts';
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await blockThirdParty(page);
|
|
});
|
|
|
|
const rowOf = (page: Page, id: string) => page.locator(`.idx a.row[data-open-sheet="${id}"]`);
|
|
|
|
async function chooseByHand(page: Page) {
|
|
await page.getByRole('button', { name: 'by hand only' }).first().click();
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'by-hand-only');
|
|
}
|
|
|
|
test('by hand only hides ai-made, some-ai and unknown; keeps by-hand', async ({ page }) => {
|
|
await openWork(page);
|
|
for (const id of [P.byHand2, P.aiMade, P.unknown]) await expect(rowOf(page, id)).toBeVisible();
|
|
await chooseByHand(page);
|
|
await expect(rowOf(page, P.byHand2)).toBeVisible();
|
|
for (const id of [P.aiMade, P.unknown]) await expect(rowOf(page, id)).toBeHidden();
|
|
await expect(page.locator('.hiddenby')).toContainText('hidden by your AI filter');
|
|
});
|
|
|
|
test('featured hidden panels show "show anyway"; archive link appears', async ({ page }) => {
|
|
await openWork(page);
|
|
await expect(page.locator('.filtered-note')).toHaveCount(6);
|
|
await expect(page.locator('.filtered-note').first()).toBeHidden();
|
|
// The archive line is in the HTML but hidden (spec 006): none of it may show, to the eye or to assistive tech.
|
|
await expect(page.locator('a[href="https://old.lucaswinther.info/"]:visible')).toHaveCount(0);
|
|
await expect(page.getByRole('link', { name: 'The 2021 site was all by hand →' })).toHaveCount(0);
|
|
await chooseByHand(page);
|
|
const visibleNotes = page.locator('.filtered-note:visible');
|
|
await expect(visibleNotes).toHaveCount(3); // the two some-ai featured projects and the unknown one
|
|
await expect(visibleNotes.first().getByRole('button', { name: 'show anyway' })).toBeVisible();
|
|
await expect(page.locator('a[href="https://old.lucaswinther.info/"]:visible').first()).toBeVisible();
|
|
});
|
|
|
|
test('personal shares (no data-ai) are never hidden', async ({ page }) => {
|
|
await setPref(page, 'by-hand-only');
|
|
await page.goto('/off-the-clock/');
|
|
await hydrated(page, 'Chips');
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'by-hand-only');
|
|
const song = page.locator(`[data-wall-card][data-id="${I.song}"]`);
|
|
expect(await song.getAttribute('data-ai')).toBeNull();
|
|
await expect(song).toBeVisible();
|
|
await expect(page.locator(`[data-wall-card][data-id="${I.byHand}"]`)).toBeVisible();
|
|
await expect(page.locator(`[data-wall-card][data-id="${I.unknown}"]`)).toBeHidden();
|
|
await expect(page.locator(`[data-wall-card][data-id="${I.someAi}"]`)).toBeHidden();
|
|
});
|
|
|
|
test('preference persists across reload', async ({ page }) => {
|
|
await openWork(page);
|
|
await chooseByHand(page);
|
|
expect(await page.evaluate((k) => localStorage.getItem(k), PREF_KEY)).toBe('by-hand-only');
|
|
await page.reload();
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'by-hand-only');
|
|
await expect(rowOf(page, P.aiMade)).toBeHidden();
|
|
await expect(page.getByRole('button', { name: 'by hand only' }).first()).toHaveAttribute('aria-pressed', 'true');
|
|
});
|
|
|
|
test('data-pref is set before first paint (no flash)', async ({ page }) => {
|
|
await page.addInitScript((k) => {
|
|
localStorage.setItem(k, 'by-hand-only');
|
|
const w = window as unknown as { __seen: { dcl?: string | null; early?: string | null } };
|
|
w.__seen = {};
|
|
// The earliest moment the attribute exists, with no <body> yet: the inline head script ran.
|
|
new MutationObserver((_m, obs) => {
|
|
const v = document.documentElement.getAttribute('data-pref');
|
|
if (v) {
|
|
w.__seen.early = `${v}|body=${!!document.body}`;
|
|
obs.disconnect();
|
|
}
|
|
}).observe(document, { attributes: true, attributeFilter: ['data-pref'], subtree: true });
|
|
document.addEventListener('DOMContentLoaded', () => (w.__seen.dcl = document.documentElement.getAttribute('data-pref')));
|
|
}, PREF_KEY);
|
|
await page.goto('/work/');
|
|
const seen = await page.evaluate(() => (window as unknown as { __seen: unknown }).__seen);
|
|
expect(seen).toEqual({ dcl: 'by-hand-only', early: 'by-hand-only|body=false' });
|
|
});
|
|
|
|
test('an invalid stored value falls back to any', async ({ page }) => {
|
|
await setPref(page, 'nonsense');
|
|
await page.goto('/work/');
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'any');
|
|
});
|
|
|
|
test('off the clock: a note counts what the filter hides; "show all" reveals for this view only', async ({ page }) => {
|
|
await setPref(page, 'by-hand-only');
|
|
await page.goto('/off-the-clock/');
|
|
await hydrated(page, 'Chips');
|
|
const hiddenIds = await page.$$eval('[data-wall-card][data-ai]:not([data-ai="by-hand"])', (cs) => cs.map((c) => (c as HTMLElement).dataset.id!));
|
|
expect(hiddenIds.length).toBeGreaterThan(0);
|
|
const note = page.locator('[data-filter-note]');
|
|
await expect(note).toBeVisible();
|
|
await expect(note).toContainText(`${hiddenIds.length} hidden by your AI filter (by hand only).`);
|
|
// The summary counts only what is on the wall.
|
|
const leadShown = await page.$$eval('.lead-wall [data-wall-card]:not([data-live])', (cs) => cs.filter((c) => c.getClientRects().length > 0).length);
|
|
await expect(page.locator('[data-wall-sum] b')).toHaveText(String(leadShown));
|
|
|
|
await note.getByRole('button', { name: 'show all' }).click();
|
|
await expect(note).toBeHidden();
|
|
for (const id of hiddenIds) await expect(page.locator(`[data-wall-card][data-id="${id}"]`)).toHaveAttribute('data-show', '');
|
|
const leadHidden = await page.$$eval('.lead-wall [data-wall-card][data-show]', (cs) => cs.map((c) => (c as HTMLElement).dataset.id!));
|
|
for (const id of leadHidden) await expect(page.locator(`.lead-wall [data-wall-card][data-id="${id}"]`)).toBeVisible();
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'by-hand-only');
|
|
expect(await page.evaluate((k) => localStorage.getItem(k), PREF_KEY)).toBe('by-hand-only');
|
|
});
|
|
|
|
test('off the clock: no note when nothing is hidden', async ({ page }) => {
|
|
await page.goto('/off-the-clock/');
|
|
await hydrated(page, 'Chips');
|
|
await expect(page.locator('[data-filter-note]')).toBeHidden();
|
|
});
|
|
|
|
test('work list: "show all" reveals rows without changing the preference', async ({ page }) => {
|
|
await openWork(page);
|
|
await chooseByHand(page);
|
|
await expect(rowOf(page, P.aiMade)).toBeHidden();
|
|
await page.locator('.hiddenby').getByRole('button', { name: 'show all' }).click();
|
|
await expect(rowOf(page, P.aiMade)).toBeVisible();
|
|
await expect(page.locator('.hiddenby')).toHaveCount(0);
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'by-hand-only');
|
|
expect(await page.evaluate((k) => localStorage.getItem(k), PREF_KEY)).toBe('by-hand-only');
|
|
});
|
|
|
|
test('with JavaScript every page marks html.js, so no-JS fallbacks stay hidden', async ({ page }) => {
|
|
for (const path of ['/', '/work/', '/off-the-clock/', '/everything/']) {
|
|
await page.goto(path);
|
|
await expect(page.locator('html')).toHaveClass(/\bjs\b/);
|
|
}
|
|
await openWork(page);
|
|
await expect(page.locator('.idx .tools')).toBeVisible();
|
|
await expect(page.locator('.pref-switch').first()).toBeVisible();
|
|
});
|
|
|
|
test('start page: the note counts each hidden entry once', async ({ page }) => {
|
|
await setPref(page, 'by-hand-only');
|
|
await page.goto('/');
|
|
const keys = await page.$$eval('#art [data-ai]', (els) => els.map((el) => (el as HTMLElement).dataset.entry ?? null));
|
|
expect(keys.length).toBeGreaterThan(0);
|
|
expect(keys).not.toContain(null); // every filterable thing on the gate names its entry
|
|
const hidden = await page.$$eval('#art [data-ai]:not([data-ai="by-hand"])', (els) => new Set(els.map((el) => (el as HTMLElement).dataset.entry)).size);
|
|
const note = page.locator('#art [data-filter-note]');
|
|
if (hidden === 0) await expect(note).toBeHidden();
|
|
else await expect(note).toContainText(`${hidden} hidden by your AI filter (by hand only).`);
|
|
});
|
|
|
|
test('issue pages: the cover line counts what the AI filter leaves', async ({ page }) => {
|
|
const WORDS = ['No', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve'];
|
|
await page.goto('/off-the-clock/issues/');
|
|
const issues = await page.$$eval('a[href^="/off-the-clock/issues/2"]', (as) => [...new Set(as.map((a) => a.getAttribute('href')!))]);
|
|
await setPref(page, 'by-hand-only');
|
|
let checked = 0;
|
|
for (const href of issues) {
|
|
await page.goto(href);
|
|
const total = await page.locator('.z-pg').count();
|
|
const hidden = await page.locator('.z-pg[data-ai]:not([data-ai="by-hand"])').count();
|
|
if (total === 0 || hidden === 0) continue;
|
|
const line = page.locator('[data-coverline]');
|
|
const shown = total - hidden;
|
|
await expect(line).toHaveText(shown === 0 ? 'Nothing this half passes your AI filter.' : new RegExp(`^${WORDS[shown]} things? this half`));
|
|
await page.locator('[data-filter-note]').getByRole('button', { name: 'show all' }).click();
|
|
await expect(line).toHaveText(new RegExp(`^${WORDS[total]} things? this half`));
|
|
checked += 1;
|
|
}
|
|
expect(checked).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('start page: the work half says what the filter hides too', async ({ page }) => {
|
|
await setPref(page, 'by-hand-only');
|
|
await page.goto('/');
|
|
const hidden = await page.locator('#tech [data-ai]:not([data-ai="by-hand"])').count();
|
|
expect(hidden).toBeGreaterThan(0);
|
|
const note = page.locator('#tech [data-filter-note]');
|
|
await expect(note).toContainText(`${hidden} hidden by your AI filter (by hand only).`);
|
|
await note.getByRole('button', { name: 'show all' }).click();
|
|
await expect(note).toBeHidden();
|
|
await expect(page.locator('#tech .t-row[data-ai="unknown"]')).toBeVisible();
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'by-hand-only');
|
|
});
|
|
|
|
for (const path of ['/', '/work/']) {
|
|
test(`a stored preference shows as pressed on ${path} from the first paint`, async ({ page }) => {
|
|
await setPref(page, 'by-hand-only');
|
|
// Block the page's own script files: what shows now comes from the HTML and its inline scripts alone,
|
|
// i.e. what the first paint has (spec 006: no flicker from "any way" to the stored choice).
|
|
await page.route('**/_astro/*.js', (r) => r.abort());
|
|
await page.goto(path);
|
|
const sw = page.locator('.pref-switch').first();
|
|
await expect(sw.getByRole('button', { name: 'by hand only' })).toHaveAttribute('aria-pressed', 'true');
|
|
for (const name of ['any way', 'not fully by AI']) {
|
|
await expect(sw.getByRole('button', { name, exact: true })).toHaveAttribute('aria-pressed', 'false');
|
|
}
|
|
await expect(sw.getByRole('link', { name: 'The 2021 site was all by hand →' })).toBeVisible();
|
|
|
|
// With its script, the switch works on top of that.
|
|
await page.unroute('**/_astro/*.js');
|
|
await page.reload();
|
|
await sw.getByRole('button', { name: 'any way' }).click();
|
|
await expect(page.locator('html')).toHaveAttribute('data-pref', 'any');
|
|
await expect(sw.getByRole('button', { name: 'any way' })).toHaveAttribute('aria-pressed', 'true');
|
|
await expect(sw.getByRole('button', { name: 'by hand only' })).toHaveAttribute('aria-pressed', 'false');
|
|
await expect(sw.getByRole('link', { name: 'The 2021 site was all by hand →' })).toBeHidden();
|
|
expect(await page.evaluate((k) => localStorage.getItem(k), PREF_KEY)).toBe('any');
|
|
});
|
|
}
|