diff --git a/tests/ui/accessibility.spec.js b/tests/ui/accessibility.spec.js new file mode 100644 index 0000000..ff954f0 --- /dev/null +++ b/tests/ui/accessibility.spec.js @@ -0,0 +1,140 @@ +// @ts-check +// Tests: A1–A5 (feature checks) and AX1–AX5 (axe scans) +const { test, expect } = require('@playwright/test'); + +// ── A1: Skip link ────────────────────────────────────────────────────────────── +test('A1: skip link targets #main-content and is first focusable element', async ({ page }) => { + await page.goto('/'); + const skipLink = page.locator('.skip-link'); + await expect(skipLink).toBeAttached(); + await expect(skipLink).toHaveAttribute('href', '#main-content'); + await expect(page.locator('#main-content')).toBeAttached(); +}); + +// ── A2: Color token contrast ─────────────────────────────────────────────────── +test('A2: contrast tokens meet WCAG AA 4.5:1 floor', async ({ page }) => { + await page.goto('/'); + const [muted, accent] = await page.evaluate(() => [ + getComputedStyle(document.documentElement).getPropertyValue('--color-ink-muted').trim(), + getComputedStyle(document.documentElement).getPropertyValue('--color-accent').trim(), + ]); + expect(muted.toLowerCase()).toBe('#90887e'); + expect(accent.toLowerCase()).toBe('#2e9880'); +}); + +// ── A3: Filter button aria-pressed + toggle aria-expanded ────────────────────── +const TRIP_URL = '/trips/italy-2026-demo'; + +test('A3a: All-content filter has aria-pressed="true" on load', async ({ page }) => { + await page.goto(TRIP_URL); + await expect(page.locator('.trip-filter-btn[data-filter="all"]')).toHaveAttribute('aria-pressed', 'true'); + await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).toHaveAttribute('aria-pressed', 'false'); + await expect(page.locator('.trip-filter-btn[data-filter="story"]')).toHaveAttribute('aria-pressed', 'false'); +}); + +test('A3b: clicking Journal filter toggles aria-pressed', async ({ page }) => { + await page.goto(TRIP_URL); + await page.click('.trip-filter-btn[data-filter="journal"]'); + await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).toHaveAttribute('aria-pressed', 'true'); + await expect(page.locator('.trip-filter-btn[data-filter="all"]')).toHaveAttribute('aria-pressed', 'false'); +}); + +test('A3c: Stats toggle has aria-expanded="false" and aria-controls on load', async ({ page }) => { + await page.goto(TRIP_URL); + await expect(page.locator('#trip-stats-toggle')).toHaveAttribute('aria-expanded', 'false'); + await expect(page.locator('#trip-stats-toggle')).toHaveAttribute('aria-controls', 'trip-stats-block'); +}); + +test('A3d: clicking Stats toggle sets aria-expanded="true" then back to false', async ({ page }) => { + await page.goto(TRIP_URL); + await page.click('#trip-stats-toggle'); + await expect(page.locator('#trip-stats-toggle')).toHaveAttribute('aria-expanded', 'true'); + await page.click('#trip-stats-toggle'); + await expect(page.locator('#trip-stats-toggle')).toHaveAttribute('aria-expanded', 'false'); +}); + +const ITALY_URL = '/trips/italy-2026-demo'; + +test('A3e: Cycling toggle has aria-expanded="false" and aria-controls on load', async ({ page }) => { + await page.goto(ITALY_URL); + await expect(page.locator('#trip-cycling-toggle')).toHaveAttribute('aria-expanded', 'false'); + await expect(page.locator('#trip-cycling-toggle')).toHaveAttribute('aria-controls', 'trip-cycling-block'); +}); + +test('A3f: clicking Cycling toggle sets aria-expanded="true" then back to false', async ({ page }) => { + await page.goto(ITALY_URL); + await page.click('#trip-cycling-toggle'); + await expect(page.locator('#trip-cycling-toggle')).toHaveAttribute('aria-expanded', 'true'); + await page.click('#trip-cycling-toggle'); + await expect(page.locator('#trip-cycling-toggle')).toHaveAttribute('aria-expanded', 'false'); +}); + +// ── A4: Photo strip keyboard navigation ─────────────────────────────────────── +test('A4a: all photo strips have role=region and aria-label', async ({ page }) => { + await page.goto('/trips/italy-2026-demo/dailies'); + const strips = page.locator('.journal-photo-strip'); + const count = await strips.count(); + if (count === 0) return; + for (let i = 0; i < count; i++) { + await expect(strips.nth(i)).toHaveAttribute('role', 'region'); + await expect(strips.nth(i)).toHaveAttribute('aria-label', 'Photo strip'); + } +}); + +test('A4b: multi-slide photo strips have accessible prev/next controls', async ({ page }) => { + await page.goto('/trips/italy-2026-demo/dailies'); + const multiCount = await page.locator('.journal-photo-strip').evaluateAll( + els => els.filter(el => parseInt(el.dataset.slides, 10) >= 2).length + ); + if (multiCount === 0) return; + await expect(page.locator('.strip-prev').first()).toBeAttached(); + await expect(page.locator('.strip-next').first()).toBeAttached(); + await expect(page.locator('.strip-prev').first()).toHaveAttribute('aria-label', 'Previous photo'); + await expect(page.locator('.strip-next').first()).toHaveAttribute('aria-label', 'Next photo'); +}); + +// ── A5: GPX delete button unique accessible names ────────────────────────────── +test('A5: GPX delete buttons have unique aria-labels per filename', async ({ page }) => { + await page.route('**/api/v1/pages**/media', async route => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: [ + { filename: 'tokyo-day1.gpx', size: 102400, modified: '2026-03-25T10:00:00Z' } + ] + }) + }); + }); + await page.goto('/gpx-manager'); + const deleteBtn = page.locator('.gpx-trip').first().locator('.gpx-delete[data-filename="tokyo-day1.gpx"]'); + await expect(deleteBtn).toBeVisible(); + await expect(deleteBtn).toHaveAttribute('aria-label', 'Delete tokyo-day1.gpx'); +}); + +// ── AX1–AX5: axe-core WCAG 2.1 AA regression scans ─────────────────────────── +const { AxeBuilder } = require('@axe-core/playwright'); + +const WCAG_TAGS = ['wcag2a', 'wcag2aa']; +const BLOCKING = ['critical', 'serious']; + +function axeScan(id, url) { + test(`${id}: ${url} passes axe WCAG 2.1 AA (critical/serious)`, async ({ page }) => { + await page.goto(url); + const results = await new AxeBuilder({ page }).withTags(WCAG_TAGS).analyze(); + const violations = results.violations.filter(v => BLOCKING.includes(v.impact)); + expect( + violations, + violations.map(v => + `[${v.impact}] ${v.id}: ${v.description}\n ` + + v.nodes.map(n => n.html).join('\n ') + ).join('\n\n') + ).toHaveLength(0); + }); +} + +axeScan('AX1', '/'); +axeScan('AX2', '/trips/italy-2026-demo'); +axeScan('AX3', '/trips/italy-2026-demo/dailies'); +axeScan('AX4', '/trips/italy-2026-demo/dailies/2026-09-01-0700-setting-off-from-campiglia.entry'); +axeScan('AX5', '/trips'); diff --git a/tests/ui/dailies.spec.js b/tests/ui/dailies.spec.js index 9ba75aa..96e01a4 100644 --- a/tests/ui/dailies.spec.js +++ b/tests/ui/dailies.spec.js @@ -1,58 +1,57 @@ // @ts-check -// Tests: T1–T5 — dailies feed and individual entry pages +// Tests: T1–T6 — dailies feed and individual entry pages const { test, expect } = require('@playwright/test'); // Known fixture entries that always exist in the repo -const KNOWN_SLUG = '2026-03-25-1540-wheels-down-narita.entry'; -const KNOWN_TITLE = 'Wheels Down at Narita'; -const KNOWN_CITY = 'Tokyo'; -const KNOWN_COUNTRY = 'Japan'; +const KNOWN_SLUG = '2026-09-01-0700-setting-off-from-campiglia.entry'; +const KNOWN_TITLE = 'Setting Off from Campiglia'; +const KNOWN_CITY = 'Campiglia Marittima'; +const KNOWN_COUNTRY = 'Italy'; -// Use two fixture entries with different dates to verify descending order -const NEWER_SLUG = '2026-06-17.entry'; // most recent fixture (June 17) -const OLDER_SLUG = '2026-03-25-1540-wheels-down-narita.entry'; // oldest fixture (March 25) +// Use two real entries from central-asia-2023 to verify descending order +const NEWER_SLUG = '2023-10-18-hunting-the-mother-of-georgia-from-above.entry'; // newest date in that trip +const OLDER_SLUG = '2023-08-28-welcome-to-my-central-asian-picture-diary.entry'; // oldest date in that trip // ── T1: Dailies page loads ───────────────────────────────────────────────────── -test('T1: /trips/japan-korea-2026/dailies loads and shows at least one entry card', async ({ page }) => { - await page.goto('/trips/japan-korea-2026/dailies'); - await expect(page.locator('.entry-card').first()).toBeVisible(); +test('T1: /trips/italy-2026-demo/dailies loads and shows at least one entry card', async ({ page }) => { + await page.goto('/trips/italy-2026-demo/dailies'); + await expect(page.locator('.journal-post').first()).toBeVisible(); await expect(page.locator('.site-header')).toBeVisible(); }); // ── T2: Entries are newest-first ────────────────────────────────────────────── -// Verify using two known fixture entries rather than all entries -// (the dailies may contain noisy test-run debris with inconsistent dates). +// Verify using two known real entries from central-asia-2023 (22 entries, stable order). test('T2: dailies shows newer entries before older entries', async ({ page }) => { - await page.goto('/trips/japan-korea-2026/dailies'); + await page.goto('/trips/central-asia-2023/dailies'); - // Both fixture entries must be visible on the page - const newerCard = page.locator(`.entry-card a[href*="${NEWER_SLUG}"]`); - const olderCard = page.locator(`.entry-card a[href*="${OLDER_SLUG}"]`); + // Use attribute selector to handle dots in slug names (CSS dots are class selectors) + const newerCard = page.locator(`.journal-post[id="entry-${NEWER_SLUG}"]`); + const olderCard = page.locator(`.journal-post[id="entry-${OLDER_SLUG}"]`); await expect(newerCard).toBeVisible(); await expect(olderCard).toBeVisible(); // The newer entry should appear higher in the DOM (lower index) const newerIdx = await newerCard.evaluate(el => { - return [...document.querySelectorAll('.entry-card')].findIndex(c => c.contains(el)); + return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id); }); const olderIdx = await olderCard.evaluate(el => { - return [...document.querySelectorAll('.entry-card')].findIndex(c => c.contains(el)); + return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id); }); expect(newerIdx).toBeLessThan(olderIdx); }); // ── T3: Individual entry page loads ─────────────────────────────────────────── -test('T3: individual entry page loads at /trips/japan-korea-2026/dailies/{slug}', async ({ page }) => { - await page.goto(`/trips/japan-korea-2026/dailies/${KNOWN_SLUG}`); +test('T3: individual entry page loads at /trips/italy-2026-demo/dailies/{slug}', async ({ page }) => { + await page.goto(`/trips/italy-2026-demo/dailies/${KNOWN_SLUG}`); await expect(page.locator('article.entry')).toBeVisible(); await expect(page.locator('.site-header')).toBeVisible(); }); // ── T4: Entry page shows title, date, and content ───────────────────────────── test('T4: entry page shows title and body content', async ({ page }) => { - await page.goto(`/trips/japan-korea-2026/dailies/${KNOWN_SLUG}`); + await page.goto(`/trips/italy-2026-demo/dailies/${KNOWN_SLUG}`); await expect(page.locator('.entry-title')).toContainText(KNOWN_TITLE); await expect(page.locator('.entry-body')).not.toBeEmpty(); await expect(page.locator('time.entry-date')).toBeVisible(); @@ -60,7 +59,20 @@ test('T4: entry page shows title and body content', async ({ page }) => { // ── T5: Entry page shows location when present ──────────────────────────────── test('T5: entry page shows city and country when set', async ({ page }) => { - await page.goto(`/trips/japan-korea-2026/dailies/${KNOWN_SLUG}`); + await page.goto(`/trips/italy-2026-demo/dailies/${KNOWN_SLUG}`); await expect(page.locator('.entry-location')).toContainText(KNOWN_CITY); await expect(page.locator('.entry-location')).toContainText(KNOWN_COUNTRY); }); + +// ── T6: Entry page has a fixed top back pill and a footer back pill ─────────────── +test('T6: entry page has fixed back pill at top and back pill in footer', async ({ page }) => { + const KNOWN_ENTRY = `/trips/italy-2026-demo/dailies/${KNOWN_SLUG}`; + await page.goto(KNOWN_ENTRY); + await expect(page.locator('article.entry')).toBeVisible(); + const topPill = page.locator('.entry-back-fixed'); + await expect(topPill).toBeVisible(); + await expect(topPill).toHaveText(/← Back/); + const footerPill = page.locator('.entry-footer .back-pill'); + await expect(footerPill).toBeVisible(); + await expect(footerPill).toHaveText(/← Back/); +}); diff --git a/tests/ui/gpx-journey.spec.js b/tests/ui/gpx-journey.spec.js index 8f6cf00..535862f 100644 --- a/tests/ui/gpx-journey.spec.js +++ b/tests/ui/gpx-journey.spec.js @@ -1,12 +1,12 @@ // @ts-check // Tests: G1–G5 — buildJourneySegments algorithm correctness -// These tests load the italy-2025 map page (which has GPX) to get MapUtils in scope, +// These tests load the italy-2026-demo map page (which has GPX) to get MapUtils in scope, // then call the functions with synthetic data via page.evaluate. // Requires demo data: run `make demo-load` before this suite. const { test, expect } = require('@playwright/test'); async function getMapUtils(page) { - await page.goto('/trips/italy-2025/map'); + await page.goto('/trips/italy-2026-demo/map'); await expect(page.locator('canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 }); } @@ -20,7 +20,7 @@ test('G1: all markers connected when no GPX files present', async ({ page }) => { lat: '44.0', lng: '12.0', force_connect: false }, { lat: '45.0', lng: '13.0', force_connect: false } ]; - return MapUtils.buildJourneySegments(entries, [], 10).length; + return MapUtils.buildJourneySegments(entries, { connectMode: 'intelligent_gpx' }, []).length; }); expect(count).toBe(1); @@ -35,7 +35,7 @@ test('G2: connector suppressed when same GPX file covers both markers', async ({ var e2 = { lat: '43.010', lng: '11.010', force_connect: false }; // Trackpoints covering both (stored as [lat, lng]) var track = [[43.000, 11.000], [43.005, 11.005], [43.010, 11.010]]; - return MapUtils.buildJourneySegments([e1, e2], [track], 10).length; + return MapUtils.buildJourneySegments([e1, e2], { connectMode: 'intelligent_gpx' }, [track]).length; }); expect(count).toBe(0); @@ -49,7 +49,7 @@ test('G3: force_connect keeps connector even when GPX covers both markers', asyn var e1 = { lat: '43.000', lng: '11.000', force_connect: false }; var e2 = { lat: '43.010', lng: '11.010', force_connect: true }; var track = [[43.000, 11.000], [43.005, 11.005], [43.010, 11.010]]; - return MapUtils.buildJourneySegments([e1, e2], [track], 10).length; + return MapUtils.buildJourneySegments([e1, e2], { connectMode: 'intelligent_gpx' }, [track]).length; }); expect(count).toBe(1); @@ -65,7 +65,7 @@ test('G4: connector kept when markers are near different GPX files', async ({ pa // Two separate files — each only covers one marker var trackA = [[43.000, 11.000], [43.005, 11.005]]; // near e1 only var trackB = [[45.000, 13.000], [45.005, 13.005]]; // near e2 only - return MapUtils.buildJourneySegments([e1, e2], [trackA, trackB], 10).length; + return MapUtils.buildJourneySegments([e1, e2], { connectMode: 'intelligent_gpx' }, [trackA, trackB]).length; }); expect(count).toBe(1); @@ -82,7 +82,7 @@ test('G5: suppressed first pair leaves one segment from e2 to e3', async ({ page var e2 = { lat: '43.010', lng: '11.010', force_connect: false }; var e3 = { lat: '45.000', lng: '13.000', force_connect: false }; var track = [[43.000, 11.000], [43.005, 11.005], [43.010, 11.010]]; // covers e1 and e2 only - var segs = MapUtils.buildJourneySegments([e1, e2, e3], [track], 10); + var segs = MapUtils.buildJourneySegments([e1, e2, e3], { connectMode: 'intelligent_gpx' }, [track]); return segs.length; }); diff --git a/tests/ui/home-highlights.spec.js b/tests/ui/home-highlights.spec.js new file mode 100644 index 0000000..ebbffe4 --- /dev/null +++ b/tests/ui/home-highlights.spec.js @@ -0,0 +1,55 @@ +// @ts-check +// Tests: H2–H5 — Between-trips highlights mode +// These tests temporarily set travelling: false in user/config/site.yaml, +// run the assertions, then restore the original value. +// Requires demo data with featured entries: run `make demo-load` first. +const { test, expect } = require('@playwright/test'); +const fs = require('fs'); +const path = require('path'); + +const SITE_YAML_PATH = path.join(__dirname, '../../user/config/site.yaml'); + +test.describe('Between-trips highlights mode', () => { + let originalSiteYaml; + + test.beforeAll(async () => { + originalSiteYaml = fs.readFileSync(SITE_YAML_PATH, 'utf8'); + const patched = originalSiteYaml.replace(/^travelling:\s*true/m, 'travelling: false'); + fs.writeFileSync(SITE_YAML_PATH, patched); + await new Promise(r => setTimeout(r, 400)); + }); + + test.afterAll(async () => { + fs.writeFileSync(SITE_YAML_PATH, originalSiteYaml); + }); + + // ── H2: Highlights grid is visible ────────────────────────────────────────── + test('H2: homepage shows highlights grid when not travelling', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.home-highlights-grid')).toBeVisible({ timeout: 10000 }); + }); + + // ── H3: Highlight cards contain trip link ──────────────────────────────────── + test('H3: highlight cards have a View-trip link', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.home-highlight-card').first()).toBeVisible({ timeout: 10000 }); + await expect(page.locator('.home-highlight-trip-link').first()).toBeVisible(); + }); + + // ── H4: Between-trips home map renders without JS errors ──────────────────── + test('H4: home map renders in between-trips mode without JS errors', async ({ page }) => { + const errors = []; + page.on('pageerror', e => errors.push(e.message)); + await page.goto('/'); + await expect(page.locator('#home-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 }); + expect(errors, 'No JS errors').toHaveLength(0); + }); + + // ── H5: CTA links to /trips ────────────────────────────────────────────────── + test('H5: "Explore all past trips" CTA links to /trips', async ({ page }) => { + await page.goto('/'); + const cta = page.locator('.home-highlights-cta'); + await expect(cta).toBeVisible({ timeout: 10000 }); + await expect(cta).toHaveAttribute('href', /\/trips/); + }); +}); diff --git a/tests/ui/home.spec.js b/tests/ui/home.spec.js new file mode 100644 index 0000000..ce1dde2 --- /dev/null +++ b/tests/ui/home.spec.js @@ -0,0 +1,10 @@ +// @ts-check +// Tests: H1 — home page journal feed +const { test, expect } = require('@playwright/test'); + +// ── H1: Home page renders inline journal posts ───────────────────────────────── +test('H1: home page shows at least one inline journal-post block', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('.journal-post').first()).toBeVisible(); + await expect(page.locator('.site-header')).toBeVisible(); +}); diff --git a/tests/ui/nav.spec.js b/tests/ui/nav.spec.js index 7d431b8..536f129 100644 --- a/tests/ui/nav.spec.js +++ b/tests/ui/nav.spec.js @@ -2,40 +2,40 @@ // Tests: N1–N5 — page loads and navigation links const { test, expect } = require('@playwright/test'); -// ── N1: /trips/japan-korea-2026/dailies renders ─────────────────────────────── -test('N1: /trips/japan-korea-2026/dailies page loads with site header', async ({ page }) => { +// ── N1: /trips/italy-2026-demo/dailies renders ─────────────────────────────── +test('N1: /trips/italy-2026-demo/dailies page loads with site header', async ({ page }) => { const errors = []; page.on('pageerror', e => errors.push(e.message)); - await page.goto('/trips/japan-korea-2026/dailies'); + await page.goto('/trips/italy-2026-demo/dailies'); await expect(page.locator('.site-header')).toBeVisible(); await expect(page).toHaveTitle(/Into the East/i); expect(errors).toHaveLength(0); }); -// ── N2: /trips/japan-korea-2026/map renders without JS errors ───────────────── -test('N2: /trips/japan-korea-2026/map page loads without JS errors', async ({ page }) => { +// ── N2: /trips/italy-2026-demo/map renders without JS errors ───────────────── +test('N2: /trips/italy-2026-demo/map page loads without JS errors', async ({ page }) => { const errors = []; page.on('pageerror', e => errors.push(e.message)); - await page.goto('/trips/japan-korea-2026/map'); + await page.goto('/trips/italy-2026-demo/map'); await expect(page.locator('.site-header')).toBeVisible(); expect(errors).toHaveLength(0); }); -// ── N3: /trips/japan-korea-2026/stats renders ───────────────────────────────── -test('N3: /trips/japan-korea-2026/stats page loads with site header', async ({ page }) => { +// ── N3: /trips/italy-2026-demo/stats renders ───────────────────────────────── +test('N3: /trips/italy-2026-demo/stats page loads with site header', async ({ page }) => { const errors = []; page.on('pageerror', e => errors.push(e.message)); - await page.goto('/trips/japan-korea-2026/stats'); + await page.goto('/trips/italy-2026-demo/stats'); await expect(page.locator('.site-header')).toBeVisible(); expect(errors).toHaveLength(0); }); // ── N4: trip page has Journal filter button (replaced nav link) ─────────────── test('N4: trip page filter bar has Journal button', async ({ page }) => { - await page.goto('/trips/japan-korea-2026'); + await page.goto('/trips/italy-2026-demo'); await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).toBeVisible(); }); diff --git a/tests/ui/stories.spec.js b/tests/ui/stories.spec.js index 20e1ebe..8c4ed9e 100644 --- a/tests/ui/stories.spec.js +++ b/tests/ui/stories.spec.js @@ -1,12 +1,12 @@ // @ts-check -// Tests: S1–S6 — story mode rendering and navigation +// Tests: S1–S7 — story mode rendering and navigation // Requires demo data: run `make demo-load` before this suite. const { test, expect } = require('@playwright/test'); -const STORIES_URL = '/trips/italy-2025/stories'; -const STORY_GALLERY = '/trips/italy-2025/stories/val-dorcia-dawn'; // gallery-led: snap-gallery × 2, chapter-break, text-only pull-quote -const STORY_SCROLLY = '/trips/italy-2025/stories/long-climb-montalcino'; // scrolly-led: scrolly-section × 2, chapter-break, pull-quote with image -const JAPAN_STORY = '/trips/japan-korea-2026/stories/the-thousand-gates'; +const STORIES_URL = '/trips/italy-2026-demo/stories'; +const STORY_GALLERY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn'; +const STORY_SCROLLY = '/trips/italy-2026-demo/stories/sorano-rock-and-time'; +const DEMO_STORY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn'; // ── S1: Stories listing shows cards ────────────────────────────────────────── test('S1: stories listing renders at least 3 story cards', async ({ page }) => { @@ -20,16 +20,12 @@ test('S1: stories listing renders at least 3 story cards', async ({ page }) => { // ── S2: Gallery-led story — hero image + snap-gallery + chapter-break + text-only pull-quote ── test('S2: gallery-led story renders hero, snap-gallery, chapter-break, text-only pull-quote', async ({ page }) => { await page.goto(STORY_GALLERY); - // Hero: real image rendered, no placeholder await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 }); await expect(page.locator('.story-hero__img-placeholder')).toHaveCount(0); - // Snap-gallery (there are two in this story) const galleries = page.locator('.pgallery'); await expect(galleries.first()).toBeVisible(); expect(await galleries.count(), 'Two snap-galleries').toBe(2); - // Chapter-break await expect(page.locator('.chapter-break')).toBeVisible(); - // Text-only pull-quote (no background image variant) await expect(page.locator('.pull-quote__inner--no-image')).toBeVisible(); }); @@ -37,11 +33,9 @@ test('S2: gallery-led story renders hero, snap-gallery, chapter-break, text-only test('S3: scrolly-led story renders two scrolly-sections and pull-quote with background image', async ({ page }) => { await page.goto(STORY_SCROLLY); await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 }); - // Two scrolly-sections const scrollySections = page.locator('.scrolly'); await expect(scrollySections.first()).toBeVisible(); expect(await scrollySections.count(), 'Two scrolly-sections').toBe(2); - // Pull-quote with background image await expect(page.locator('.pull-quote__bg')).toBeVisible(); }); @@ -57,19 +51,28 @@ test('S4: scrolly story page loads without JS errors', async ({ page }) => { // ── S5: Back button returns to stories listing ──────────────────────────────── test('S5: back button navigates back to stories listing', async ({ page }) => { - // Establish history: listing → story → back await page.goto(STORIES_URL); await page.locator('.story-card').first().click(); await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 }); await page.locator('.story-escape').click(); - // After history.back(), URL should be the stories listing - await expect(page).toHaveURL(/italy-2025\/stories$/); + await expect(page).toHaveURL(/italy-2026-demo\/stories$/); await expect(page.locator('.story-card').first()).toBeVisible(); }); -// ── S6: Japan story — cross-trip hero image sanity check ───────────────────── -test('S6: Japan story renders hero image without placeholder', async ({ page }) => { - await page.goto(JAPAN_STORY); +// ── S6: Demo story — hero image sanity check ───────────────────────────────── +test('S6: demo story renders hero image without placeholder', async ({ page }) => { + await page.goto(DEMO_STORY); await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 }); await expect(page.locator('.story-hero__img-placeholder')).toHaveCount(0); }); + +// ── S7: Story body back link is styled as a back-pill ──────────────────────── +test('S7: story body back link has back-pill class', async ({ page }) => { + await page.goto('/trips/italy-2026-demo/stories/val-dorcia-at-dawn'); + await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 }); + await page.evaluate(() => window.scrollBy(0, window.innerHeight * 1.5)); + await page.waitForTimeout(300); + const bodyBack = page.locator('.story-footer .back-pill'); + await expect(bodyBack).toBeAttached(); + await expect(bodyBack).toHaveText(/← Back/); +});