diff --git a/playwright.config.ts b/playwright.config.ts index ee01dfd..1745d0a 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,12 +5,33 @@ import { defineConfig } from '@playwright/test'; * database). Because it needs those secrets, e2e is a local/manual step and is * intentionally not part of the CI workflow. */ + +/** + * A port of this suite's own, deliberately not Vite's default. + * + * On 5173 the suite once ran end to end against somebody else's application: + * another Vite project had taken the default port first, `reuseExistingServer` + * adopted it without checking whose it was, and all seventeen tests failed + * against a stranger's HTML — including the ones about `robots.txt` and + * security headers, which pointed nowhere near the truth. The dangerous + * direction is the other one: a reused server running stale code can report + * green for a failure that is really there. + * + * A port nothing else defaults to is what removes that collision. `--strictPort` + * closes the second half of it — without it Vite answers a busy port by quietly + * moving to the next one, and the suite would then be pointed at nothing. + */ +const PORT = 5273; + export default defineConfig({ testMatch: '**/*.e2e.{ts,js}', - use: { baseURL: 'http://localhost:5173' }, + use: { baseURL: `http://localhost:${PORT}` }, webServer: { - command: 'npm run dev', - port: 5173, + command: `npm run dev -- --port ${PORT} --strictPort`, + port: PORT, + // Kept for local re-runs, which are otherwise a fresh server every time. + // It is safe here in a way it was not on 5173: nothing but this suite ever + // listens on this port, so the server being reused is always our own. reuseExistingServer: !process.env.CI } }); diff --git a/src/lib/components/media/ContinueWatching.svelte b/src/lib/components/media/ContinueWatching.svelte index 49c6353..ba6e9a2 100644 --- a/src/lib/components/media/ContinueWatching.svelte +++ b/src/lib/components/media/ContinueWatching.svelte @@ -6,7 +6,7 @@ import { getSeasonProgress } from '$lib/domain/progress'; import { progressNote } from '$lib/domain/episodes'; import { posterUrl } from '$lib/format/tmdb-image'; - import type { WatchlistItem } from '$lib/server/db/schema'; + import type { SavedTitle } from '$lib/domain/watchlist'; /** * "What was I in the middle of?" — the first question anyone opens a watchlist @@ -19,9 +19,9 @@ * row of vertical space. */ interface Props { - items: WatchlistItem[]; - onSelect: (item: WatchlistItem) => void; - onSetSeasons: (item: WatchlistItem) => SubmitFunction; + items: SavedTitle[]; + onSelect: (item: SavedTitle) => void; + onSetSeasons: (item: SavedTitle) => SubmitFunction; } let { items, onSelect, onSetSeasons }: Props = $props(); diff --git a/src/lib/components/media/MediaCard.svelte b/src/lib/components/media/MediaCard.svelte index 3a83d25..455c639 100644 --- a/src/lib/components/media/MediaCard.svelte +++ b/src/lib/components/media/MediaCard.svelte @@ -22,35 +22,21 @@ watched?: boolean; /** Extra line under the title, e.g. season progress. */ note?: string; - /** - * A TV season that has not aired, badged on the poster the same way an - * unreleased film is — the visual language for "not watchable yet" should - * not depend on whether the title is a film or a show. - */ + /** An unaired season, badged like an unreleased film: same language for both. */ upcomingSeason?: { number: number; label: string } | null; /** - * Set on the tiles that land above the fold. - * - * `loading="lazy"` is right for a long grid but wrong for its first row: - * lazy images are invisible to the preload scanner and fetched at low - * priority, so applying it to every poster pushes the one that *is* the - * LCP element to the back of the queue. - * - * Measured against the production build on Discover: LCP 972ms -> ~885ms - * and FCP 640ms -> ~570ms. Local numbers, so the real-network gap is - * likely wider — priority hints matter most under bandwidth contention. + * Set on the tiles above the fold. Lazy images are invisible to the preload + * scanner, so lazy-loading the first row delays the very poster that is the + * LCP element. Measured on Discover: LCP 972ms -> 885ms, FCP 640ms -> 570ms. */ priority?: boolean; /** When provided, the poster and title become clickable to open details. */ onSelect?: () => void; /** - * Where the poster and title lead, when following the tile means going - * somewhere rather than opening a sheet over what is already here. - * - * A real anchor rather than a button that navigates, because a grid of - * links is a grid people open in background tabs — which is precisely what - * somebody does with a list they have been sent. Ignored when `onSelect` is - * given; a tile has one behaviour. + * Where the tile leads, when following it means navigating rather than + * opening a sheet. A real anchor, so a shared list can be opened into + * background tabs. Ignored when `onSelect` is given; a tile has one + * behaviour. */ href?: string; /** Action controls rendered in the card footer (buttons, forms, badges). */ @@ -75,12 +61,7 @@ /** Which of the two follow behaviours this tile has, if either. */ const interactive = $derived(Boolean(onSelect || href)); - /** - * The title's own styling, shared by the button and the anchor forms of it. - * - * Named rather than repeated: the two differ only in which element they are, - * and a class list copied into both is one that will be edited in one. - */ + /** Shared by the button and anchor forms of the title, so neither drifts. */ const TITLE_LINK = '-my-3 block cursor-pointer py-3 text-left text-sm leading-snug font-semibold text-ink transition-colors duration-200 hover:text-brand-hi'; @@ -88,8 +69,7 @@ const year = $derived(releaseYear(releaseDate)); const rating = $derived(voteAverage ? voteAverage.toFixed(1) : null); - // TMDB returns titles that are still in production alongside released ones, - // so the card has to be able to say "not out yet — here's when". + // TMDB lists in-production titles alongside released ones. const release = $derived(getReleaseInfo(releaseDate)); const unreleased = $derived(release.state !== 'released'); @@ -133,18 +113,10 @@ > {#if interactive} {#if onSelect}