// @ts-check // Tests: U3 — trip-list card one-liner + retina cover (R5, R6, R7, R14) // R11 (set-but-unresolvable cover_image falls back) shares the exact else-branch // exercised by the R7/AE3 fallback test below; it is covered by construction in // the shared cover macro rather than by a dedicated fixture here. const { test, expect } = require('@playwright/test'); const DEMO_HREF = '/trips/italy-2026-demo'; // has a tagline, no cover_image (entry-image fallback) const NO_TAGLINE_HREF = '/trips/slovenia-2024'; // a real trip with no tagline const demoCard = (page) => page.locator(`.trip-card[href="${DEMO_HREF}"]`); // ── R5: one-liner renders between the title and the meta line ────────────────── test('U3/R5: card with a tagline shows a one-liner between title and meta', async ({ page }) => { await page.goto('/trips'); const card = demoCard(page); const tagline = card.locator('.trip-card-tagline'); await expect(tagline).toBeVisible(); await expect(tagline).toHaveText(/southern Tuscany by bike/); const order = await card.evaluate((el) => Array.from(el.children).map((c) => c.className.split(' ')[0]) ); expect(order.indexOf('trip-card-title')).toBeLessThan(order.indexOf('trip-card-tagline')); expect(order.indexOf('trip-card-tagline')).toBeLessThan(order.indexOf('trip-card-meta')); }); // ── R5/AE1: a card with no tagline renders no one-liner element ──────────────── test('U3/R5/AE1: card without a tagline renders no one-liner element', async ({ page }) => { await page.goto('/trips'); const card = page.locator(`.trip-card[href="${NO_TAGLINE_HREF}"]`); await expect(card).toBeVisible(); await expect(card.locator('.trip-card-tagline')).toHaveCount(0); }); // ── 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'); // 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 ──── test('U3/R7/AE3: card with no cover_image uses the first journal entry image', async ({ page }) => { await page.goto('/trips'); // The demo trip sets cover_image: '' so the cover comes from the fallback. const img = demoCard(page).locator('.trip-card-cover img'); await expect(img).toBeVisible(); const src = await img.getAttribute('src'); expect(src).toMatch(/\/images\/.+\.(jpg|jpeg|png|webp)/i); }); // ── R14: cover alt text equals the trip title ───────────────────────────────── test('U3/R14: card cover alt equals the trip title', async ({ page }) => { await page.goto('/trips'); const img = demoCard(page).locator('.trip-card-cover img'); await expect(img).toHaveAttribute('alt', 'Tuscany 2026'); });