Files
Lucas WintherandClaude Opus 5.5 35415e774d feat(site): preference switch as plain HTML, pressed from the first paint
The React PreferenceSwitch island is replaced by PreferenceSwitch.astro: static
markup, an inline script that presses the stored choice before first paint, and
a page script for clicks and lw:pref. Used on the start page and the work bar.

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

71 lines
2.8 KiB
TypeScript

import type { Page } from '@playwright/test';
import config from '../playwright.config.ts';
export const PREF_KEY = 'lw:projects-made';
// Fixture ids (see fixtures/content): levels by-hand / some-ai / ai-made / unknown.
export const P = {
byHand: 'example-compute-portal', // featured 1 (not in the list)
byHand2: 'demo-course-planner',
someAi: 'fixture-dashboard', // featured 2
aiMade: 'sample-garden-bot',
unknown: 'placeholder-notes',
draft: 'draft-unfinished-thing',
};
export const I = {
soundtrackFilm: 'fixture-soundtrack-film', // [film-tv, music], spotify media url
song: 'example-song', // youtube link, no ai (a "share")
byHand: 'own-drawing',
unknown: 'fixture-life',
someAi: 'synthetic-life-note',
};
// Keeps tests offline: only the local server is reachable. Returns the blocked URLs.
const servers = Array.isArray(config.webServer) ? config.webServer : config.webServer ? [config.webServer] : [];
const LOCAL = new Set(servers.map((s) => ('url' in s && s.url ? new URL(s.url).origin : '')));
export async function blockThirdParty(page: Page): Promise<string[]> {
const blocked: string[] = [];
await page.route(/^https?:\/\//, (r) => {
const url = r.request().url();
if (LOCAL.has(new URL(url).origin)) return r.continue();
// The analytics script is the one allowed third-party request; it is aborted but not counted.
if (!url.startsWith('https://analytics.lucaswinther.info/')) blocked.push(url);
return r.abort();
});
return blocked;
}
export async function setPref(page: Page, value: string) {
await page.addInitScript(([k, v]) => localStorage.setItem(k!, v!), [PREF_KEY, value]);
}
// Waits until the named islands have hydrated (Astro drops the `ssr` attribute), scrolling
// client:visible ones into view first.
export async function hydrated(page: Page, ...names: string[]) {
for (const n of names) {
await page.evaluate((name) => {
const el = document.querySelector(`astro-island[component-url*="/${name}."]`);
(el?.firstElementChild ?? el?.parentElement)?.scrollIntoView({ block: 'center' });
}, n);
await page.waitForFunction((name) => {
const el = document.querySelector(`astro-island[component-url*="/${name}."]`);
return !!el && !el.hasAttribute('ssr');
}, n);
}
}
// Open /work/ with the sheet, the list and the preference switch live. The switch is a plain module
// script (spec 006), which has run by the time goto resolves on `load`.
export async function openWork(page: Page) {
await page.goto('/work/');
await hydrated(page, 'ProjectSheet', 'ProjectList');
await page.evaluate(() => scrollTo(0, 0));
}
// Open /off-the-clock/ with the chips live.
export async function openPersonal(page: Page) {
await page.goto('/off-the-clock/');
await hydrated(page, 'Chips');
}