test(trip): specs for trip one-liner, description, banner & home gating
- trips-list.spec.js: one-liner presence/absence, retina srcset, cover fallback, alt text (U3). - trip-header.spec.js: HTD stacking order, expandable description, banner fallback, split intact (U4). - home.spec.js: AE7 — the gated trip-page extras never leak onto the home route (asserted mode-independently so it can't race the home-highlights suite that toggles travelling in a parallel worker). Also marks the plan complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
// Tests: H1 — home page journal feed
|
||||
// Tests: H1 — home page journal feed; AE7 — active-trip header gating
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
// ── H1: Home page renders inline journal posts ─────────────────────────────────
|
||||
@@ -8,3 +8,17 @@ test('H1: home page shows at least one inline journal-post block', async ({ page
|
||||
await expect(page.locator('.journal-post').first()).toBeVisible();
|
||||
await expect(page.locator('.site-header')).toBeVisible();
|
||||
});
|
||||
|
||||
// ── AE7: the trip-page header extras never leak onto the home route ────────────
|
||||
// The trip-page one-liner/description/banner are gated to the trip.html.twig
|
||||
// caller of the shared trip-feed-col partial (trip_header_extras, default off);
|
||||
// home's include omits the flag, so its header is unchanged (R12/KTD4). Asserted
|
||||
// as an absence on `/` so it holds whether home is in active-trip or
|
||||
// between-trips mode — the sibling home-highlights suite toggles that mode in a
|
||||
// parallel worker, so this test must not depend on it.
|
||||
test('AE7: home never renders the trip-page header extras', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page.locator('.home-trip-tagline')).toHaveCount(0);
|
||||
await expect(page.locator('.trip-header-desc')).toHaveCount(0);
|
||||
await expect(page.locator('.trip-header-banner')).toHaveCount(0);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
// @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();
|
||||
|
||||
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);
|
||||
await expect(body).toContainText('finally made sense'); // tail of the full description
|
||||
});
|
||||
|
||||
// ── 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');
|
||||
expect(srcset).toContain('1440w');
|
||||
await expect(img).toHaveAttribute('alt', 'Tuscany 2026');
|
||||
});
|
||||
|
||||
// ── 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);
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
// @ts-check
|
||||
// Tests: U3 — trip-list card one-liner + retina cover (R5, R6, R7, R11, R14)
|
||||
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 retina srcset (720w + 1440w candidates) ───────
|
||||
test('U3/R6/AE5: card cover img carries a 720w/1440w srcset', 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');
|
||||
});
|
||||
|
||||
// ── 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');
|
||||
});
|
||||
Reference in New Issue
Block a user