// @ts-check // Tests: U4 — trip-page in-column header extras (R8, R9, R10, R13) const { test, expect } = require('@playwright/test'); const TRIP_URL = '/trips/italy-2026-demo'; const topOf = async (locator) => (await locator.boundingBox()).y; // ── R8/R9: extras render in HTD order, banner sits above the filter bar ──────── test('U4/R8+R9: one-liner, description and banner stack in order above the filter bar', async ({ page }) => { await page.goto(TRIP_URL); const header = page.locator('.home-trip-header'); await expect(header.locator('.home-trip-tagline')).toHaveText(/southern Tuscany by bike/); const yTitle = await topOf(page.locator('.home-trip-name')); const yTag = await topOf(page.locator('.home-trip-tagline')); const yCounts = await topOf(page.locator('.home-trip-counts')); const yDesc = await topOf(page.locator('.trip-header-desc')); const yBanner = await topOf(page.locator('.trip-header-banner')); const yFilter = await topOf(page.locator('.trip-filter-bar')); expect(yTitle).toBeLessThan(yTag); // one-liner directly below the title expect(yTag).toBeLessThan(yCounts); expect(yCounts).toBeLessThan(yDesc); // description below the counts expect(yDesc).toBeLessThan(yBanner); // banner below the description expect(yBanner).toBeLessThan(yFilter); // ...and above the filter bar (R9) }); // ── R8/R13: description shows a collapsed preview and expands on demand ──────── test('U4/R13: description is clamped to a preview and expands to full text', async ({ page }) => { await page.goto(TRIP_URL); const desc = page.locator('.trip-header-desc'); const body = page.locator('.trip-header-desc-body'); const btn = page.locator('.trip-header-desc-toggle'); await expect(desc).toHaveAttribute('data-collapsed', 'true'); await expect(btn).toBeVisible(); // Collapsed: the body is genuinely clamped — visible height is shorter than // its full content (the max-height:4.8em preview actually hides overflow). const clampedWhenCollapsed = await body.evaluate((el) => el.clientHeight < el.scrollHeight); expect(clampedWhenCollapsed).toBe(true); const collapsedH = (await body.boundingBox()).height; await btn.click(); await expect(desc).toHaveAttribute('data-collapsed', 'false'); await expect(btn).toHaveText('Show less'); const expandedH = (await body.boundingBox()).height; expect(expandedH).toBeGreaterThan(collapsedH); // Expanded: the clamp is gone — the full text is now actually visible, not // merely present in the DOM (which it was even while collapsed). const unclampedWhenExpanded = await body.evaluate((el) => el.clientHeight >= el.scrollHeight - 1); expect(unclampedWhenExpanded).toBe(true); }); // ── R9/AE3: banner uses the first journal image (no cover_image set) ─────────── test('U4/R9/AE3: banner falls back to the first journal entry image', async ({ page }) => { await page.goto(TRIP_URL); const img = page.locator('.trip-header-banner img'); await expect(img).toBeVisible(); const srcset = await img.getAttribute('srcset'); expect(srcset).toContain('720w'); // 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'); }); // ── 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 ─────────── test('U4/R10/AE6: map+journal split renders with the extras inside the feed column', async ({ page }) => { await page.goto(TRIP_URL); await expect(page.locator('.home-layout')).toBeVisible(); await expect(page.locator('.home-layout > .home-map-col')).toBeVisible(); await expect(page.locator('.home-layout > .home-feed-col')).toBeVisible(); // extras live inside the feed column, not as a new header above the split await expect(page.locator('.home-feed-col .trip-header-banner')).toHaveCount(1); await expect(page.locator('.home-feed-col .home-trip-tagline')).toHaveCount(1); // the header extras never appear outside the two-column layout await expect(page.locator('body > .trip-header-banner, .home-layout ~ .trip-header-banner')).toHaveCount(0); });