From 5276768e12ea24ab63fc24ee3567e28dbe0ad55d Mon Sep 17 00:00:00 2001 From: Mischa Date: Tue, 7 Jul 2026 23:39:32 +0200 Subject: [PATCH] test(trip-cover): cover-strip + retina-cap regression; bump user pin - trip-header/trips-list specs now assert the cropZoom wide-strip aspect and the all-or-nothing retina rule (1x-only for sub-2w sources), and a new regression on us-canada-mex-2024 covers the reported portrait-blur - remove the orphaned AE4 test (no-photos-demo fixture was deleted) - backlog: full-res pixelfed reimport (luxury quality item) - plan: record the cover-quality follow-up + AE4 fixture removal - bump user pin -> 536ca20 (trip-description-hero cover fix) Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K --- docs/working/backlog.md | 6 +++ .../2026-07-05-trip-description-and-hero.md | 24 +++++++++-- tests/ui/trip/trip-header.spec.js | 42 +++++++++++++------ tests/ui/trip/trips-list.spec.js | 15 +++++-- user | 2 +- 5 files changed, 68 insertions(+), 21 deletions(-) diff --git a/docs/working/backlog.md b/docs/working/backlog.md index 788c0c4..68b3035 100644 --- a/docs/working/backlog.md +++ b/docs/working/backlog.md @@ -15,6 +15,12 @@ Ideas and improvements not yet planned or scheduled. --- +## Content quality — luxury improvements (much later) + +- [ ] **Re-import pixelfed photos at full resolution** — the current import pulled pixelfed's optimised web renditions, so imported images cap at ~1440px on the long edge (portraits are 700–1200px wide). This is fine for the feed and 1x banners, but the retina cover 2x only kicks in for genuinely wide (≥1440px) sources, so auto-picked trip banners are currently 1x-only. Find the original high-quality versions in the local filesystem and re-import them (or point the pipeline at the originals rather than the pixelfed web renditions). Purely a quality upgrade — no functional gap; future content shot/stored at full res won't have this ceiling. + +--- + ## GPX Manager (`/gpx-manager`) - [ ] **Polish the UI** — the current design is functional but bare; align with the Field Notes aesthetic, add better empty states, drag-and-drop upload area diff --git a/docs/working/plans/2026-07-05-trip-description-and-hero.md b/docs/working/plans/2026-07-05-trip-description-and-hero.md index fe83e85..4062499 100644 --- a/docs/working/plans/2026-07-05-trip-description-and-hero.md +++ b/docs/working/plans/2026-07-05-trip-description-and-hero.md @@ -274,10 +274,26 @@ intentionally-deferred findings. non-image selection (e.g. a `.gpx` from the trip page media) can no longer route a non-image Medium into `cropResize`. Also hardens R11. - Test quality: replaced a vacuous `toContainText` in the description-clamp - spec with real clamp/un-clamp assertions; added the missing **AE4** coverage - (trip with no cover + no entry images → text-only header, no banner) via a - `no-photos-demo` fixture; corrected an R11 over-claim in the trips-list spec - header comment. + spec with real clamp/un-clamp assertions; corrected an R11 over-claim in the + trips-list spec header comment. + +**Follow-up (2026-07-07)** + +- **Banner/card cover quality fix.** The macro used `cropResize`, which + *fits-inside* preserving aspect ratio — so a portrait fallback source was + handed back as a ~165px sliver that the `object-fit:cover` box then upscaled + into a blur (reported on `us-canada-mex-2024`). Switched to **`cropZoom`** + (crop-to-fill → a real w×h cover strip). Retina is now **all-or-nothing**: the + 2x `srcset` descriptor is emitted only when the source is genuinely ≥2×w + (`cover.width >= 2w`), else 1x-only — no upscaling, no intermediate widths. + Note: imported pixelfed photos cap at ~1440px wide, so auto-picked covers are + usually 1x-only; see `docs/working/backlog.md` (full-res re-import, luxury). +- **AE4 fixture removed.** The `no-photos-demo` fixture (and its browser test) + was deleted at the user's request — it surfaced as stray demo content in the + trip list. AE4 (no cover + no images → no banner) is a trivial else-branch of + the shared macro's `{% if cover %}` guard, covered by construction alongside + the R7/AE3 fallback tests. A regression test for the reported portrait-blur + bug now lives in `trip-header.spec.js` against `us-canada-mex-2024`. **Intentionally deferred — explicit plan override (do not re-flag)** diff --git a/tests/ui/trip/trip-header.spec.js b/tests/ui/trip/trip-header.spec.js index aaadf93..325c669 100644 --- a/tests/ui/trip/trip-header.spec.js +++ b/tests/ui/trip/trip-header.spec.js @@ -3,7 +3,6 @@ const { test, expect } = require('@playwright/test'); const TRIP_URL = '/trips/italy-2026-demo'; -const NO_PHOTOS_URL = '/trips/no-photos-demo'; // no cover_image, one image-less entry const topOf = async (locator) => (await locator.boundingBox()).y; @@ -62,21 +61,38 @@ test('U4/R9/AE3: banner falls back to the first journal entry image', async ({ p await expect(img).toBeVisible(); const srcset = await img.getAttribute('srcset'); expect(srcset).toContain('720w'); - expect(srcset).toContain('1440w'); + // The 1200px landscape source can't supply a non-upscaled 2x (needs ≥1440), + // so the retina descriptor is omitted entirely — 1x only, no blurry upscale + // and no odd intermediate width. + expect(srcset).not.toContain('1440w'); + expect(srcset).not.toContain('1200w'); + // cropZoom hands over a wide cover strip (~3.27:1); the old cropResize gave + // a fit-inside image the object-fit:cover box then upscaled into a blur. + const ratio = await img.evaluate((el) => new Promise((res) => { + const done = () => res(el.naturalWidth / el.naturalHeight); + el.complete && el.naturalWidth ? done() : el.addEventListener('load', done, { once: true }); + })); + expect(ratio).toBeGreaterThan(3); await expect(img).toHaveAttribute('alt', 'Tuscany 2026'); }); -// ── R9/AE4: no cover_image and no entry images → text-only header, no banner ─── -test('U4/R9/AE4: trip with no cover and no entry images renders no banner strip', async ({ page }) => { - await page.goto(NO_PHOTOS_URL); - // The text extras still render (the header is not blank)... - await expect(page.locator('.home-trip-name')).toHaveText('Notes from the Road 2027'); - await expect(page.locator('.home-trip-tagline')).toBeVisible(); - // ...and there is a journal entry, so the empty-cover path is the macro's - // fallback loop finding no image — not simply an empty trip. - await expect(page.locator('.feed .journal-post')).toHaveCount(1); - // With no resolvable cover, the banner strip is omitted entirely (AE4). - await expect(page.locator('.trip-header-banner')).toHaveCount(0); +// ── Regression: a portrait fallback source (the reported us-canada-mex-2024 +// banner) must render a sharp wide cover strip, not a blurry upscaled sliver ─ +test('U4/R9: a portrait fallback source renders a wide cover strip, not a sliver', async ({ page }) => { + await page.goto('/trips/us-canada-mex-2024'); + const img = page.locator('.trip-header-banner img'); + await expect(img).toBeVisible(); + const srcset = await img.getAttribute('srcset'); + expect(srcset).toContain('720w'); + // 1013px portrait source — nowhere near ≥1440 — so no upscaled 2x candidate. + expect(srcset).not.toContain('1440w'); + // cropZoom crops the tall portrait into a wide cover strip (~3.27:1); the old + // cropResize fit it inside as a ~165px sliver the box then upscaled to a blur. + const ratio = await img.evaluate((el) => new Promise((res) => { + const done = () => res(el.naturalWidth / el.naturalHeight); + el.complete && el.naturalWidth ? done() : el.addEventListener('load', done, { once: true }); + })); + expect(ratio).toBeGreaterThan(3); }); // ── R10/AE6: the map+journal split is intact with no header above it ─────────── diff --git a/tests/ui/trip/trips-list.spec.js b/tests/ui/trip/trips-list.spec.js index 1d72518..1405fb9 100644 --- a/tests/ui/trip/trips-list.spec.js +++ b/tests/ui/trip/trips-list.spec.js @@ -33,13 +33,22 @@ test('U3/R5/AE1: card without a tagline renders no one-liner element', async ({ await expect(card.locator('.trip-card-tagline')).toHaveCount(0); }); -// ── R6/AE5: card cover exposes a retina srcset (720w + 1440w candidates) ─────── -test('U3/R6/AE5: card cover img carries a 720w/1440w srcset', async ({ page }) => { +// ── R6/AE5: card cover exposes a 1x srcset; 2x only when the source is ≥2×w ──── +test('U3/R6/AE5: card cover img carries a 720w srcset, 2x omitted for a narrow source', async ({ page }) => { await page.goto('/trips'); const img = demoCard(page).locator('.trip-card-cover img'); const srcset = await img.getAttribute('srcset'); expect(srcset).toContain('720w'); - expect(srcset).toContain('1440w'); + // The 1200px source can't supply a non-upscaled 2x (needs ≥1440), so the + // retina descriptor is omitted — 1x only, no upscale, no intermediate width. + expect(srcset).not.toContain('1440w'); + expect(srcset).not.toContain('1200w'); + // cropZoom cover strip (~3:1), not a cropResize fit-inside sliver. + const ratio = await img.evaluate((el) => new Promise((res) => { + const done = () => res(el.naturalWidth / el.naturalHeight); + el.complete && el.naturalWidth ? done() : el.addEventListener('load', done, { once: true }); + })); + expect(ratio).toBeGreaterThan(2.5); }); // ── R7/AE3: with no cover_image set, the card falls back to a journal image ──── diff --git a/user b/user index 4aa9ae9..536ca20 160000 --- a/user +++ b/user @@ -1 +1 @@ -Subproject commit 4aa9ae9b23d4032f016e6f84d179b632aac29662 +Subproject commit 536ca2085fab1b2dc9ea2d563d82a658bf680dcb