test: fix stale trip-slug references; add home, highlights, a11y specs

This commit is contained in:
2026-06-21 16:12:42 +02:00
parent d507d04825
commit 508fcbdbe8
7 changed files with 277 additions and 57 deletions
+140
View File
@@ -0,0 +1,140 @@
// @ts-check
// Tests: A1A5 (feature checks) and AX1AX5 (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');
});
// ── AX1AX5: 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');
+35 -23
View File
@@ -1,58 +1,57 @@
// @ts-check
// Tests: T1T5 — dailies feed and individual entry pages
// Tests: T1T6 — 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/);
});
+7 -7
View File
@@ -1,12 +1,12 @@
// @ts-check
// Tests: G1G5 — 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;
});
+55
View File
@@ -0,0 +1,55 @@
// @ts-check
// Tests: H2H5 — 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/);
});
});
+10
View File
@@ -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();
});
+10 -10
View File
@@ -2,40 +2,40 @@
// Tests: N1N5 — 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();
});
+20 -17
View File
@@ -1,12 +1,12 @@
// @ts-check
// Tests: S1S6 — story mode rendering and navigation
// Tests: S1S7 — 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/);
});