test: re-point suite off retired standalone views

Phase-1 fallout: delete tests for deleted pages, re-point moved-behavior
tests to the trip/home page where the content now lives.

- maps.spec: keep M4/M7/M8 (home+trip maps); drop M1-3,5,6,9-11 (/map,
  /dailies & /stories mini-maps)
- map-ux: re-point MUX4/5 sort toggle to #trip-sort-toggle on trip page
- nav: N1->trip page, N2->home, drop N3 (/stats) + N5 (obsolete skip)
- dailies: T1->trip page; T2->trip page with oldest-first assertion;
  fix T3-T6 stale entry-detail selectors (.entry-* -> .journal-post-*,
  pre-existing failures from an earlier entry redesign)
- stories: S1/S5 -> trip feed story cards ([data-type=story])
- gpx-journey: load MapUtils from the trip page instead of /map
- a11y: A4 photo strips -> trip page; drop AX3 (covered by AX2)
- post/helpers: verify posted entry on the trip page (ACTIVE_TRIP_URL)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 13:58:27 +02:00
co-authored by Claude Opus 4.8
parent a2457d6402
commit 7407129812
9 changed files with 80 additions and 189 deletions
+2 -3
View File
@@ -69,7 +69,7 @@ test('A3f: clicking Cycling toggle sets aria-expanded="true" then back to 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');
await page.goto('/trips/italy-2026-demo');
const strips = page.locator('.journal-photo-strip');
const count = await strips.count();
if (count === 0) return;
@@ -80,7 +80,7 @@ test('A4a: all photo strips have role=region and aria-label', async ({ page }) =
});
test('A4b: multi-slide photo strips have accessible prev/next controls', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/dailies');
await page.goto('/trips/italy-2026-demo');
const multiCount = await page.locator('.journal-photo-strip').evaluateAll(
els => els.filter(el => parseInt(el.dataset.slides, 10) >= 2).length
);
@@ -133,7 +133,6 @@ function axeScan(id, url) {
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');
+17 -15
View File
@@ -12,17 +12,19 @@ const KNOWN_COUNTRY = 'Italy';
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/italy-2026-demo/dailies loads and shows at least one entry card', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/dailies');
// ── T1: Trip page journal feed loads ──────────────────────────────────────────
// The journal feed moved onto the trip page when the standalone /dailies view was retired.
test('T1: trip page loads and shows at least one journal entry card', async ({ page }) => {
await page.goto('/trips/italy-2026-demo');
await expect(page.locator('.journal-post').first()).toBeVisible();
await expect(page.locator('.site-header')).toBeVisible();
});
// ── T2: Entries are newest-first ──────────────────────────────────────────────
// ── T2: Trip feed default order is oldest-first ───────────────────────────────
// The trip page sorts the feed oldest→newest by default (home uses newest-first).
// 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/central-asia-2023/dailies');
test('T2: trip feed shows older entries before newer entries (oldest-first default)', async ({ page }) => {
await page.goto('/trips/central-asia-2023');
// Use attribute selector to handle dots in slug names (CSS dots are class selectors)
const newerCard = page.locator(`.journal-post[id="entry-${NEWER_SLUG}"]`);
@@ -31,7 +33,7 @@ test('T2: dailies shows newer entries before older entries', async ({ page }) =>
await expect(newerCard).toBeVisible();
await expect(olderCard).toBeVisible();
// The newer entry should appear higher in the DOM (lower index)
// The older entry should appear higher in the DOM (lower index)
const newerIdx = await newerCard.evaluate(el => {
return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id);
});
@@ -39,36 +41,36 @@ test('T2: dailies shows newer entries before older entries', async ({ page }) =>
return [...document.querySelectorAll('.journal-post')].findIndex(c => c.id === el.id);
});
expect(newerIdx).toBeLessThan(olderIdx);
expect(olderIdx).toBeLessThan(newerIdx);
});
// ── T3: Individual entry page loads ───────────────────────────────────────────
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('article.journal-post')).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/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();
await expect(page.locator('.journal-post-title')).toContainText(KNOWN_TITLE);
await expect(page.locator('.journal-post-body')).not.toBeEmpty();
await expect(page.locator('.journal-post-meta time')).toBeVisible();
});
// ── T5: Entry page shows location when present ────────────────────────────────
test('T5: entry page shows city and country when set', async ({ page }) => {
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);
await expect(page.locator('.journal-post-location')).toContainText(KNOWN_CITY);
await expect(page.locator('.journal-post-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();
await expect(page.locator('article.journal-post')).toBeVisible();
const topPill = page.locator('.entry-back-fixed');
await expect(topPill).toBeVisible();
await expect(topPill).toHaveText(/← Back/);
+4 -4
View File
@@ -1,13 +1,13 @@
// @ts-check
// Tests: G1G5 — buildJourneySegments algorithm correctness
// 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.
// These tests load the italy-2026-demo trip page (which has GPX + the map bundle) 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-2026-demo/map');
await expect(page.locator('canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
await page.goto('/trips/italy-2026-demo');
await expect(page.locator('#trip-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
}
// G1: No GPX → all pairs connected in one segment
+10 -9
View File
@@ -62,19 +62,20 @@ const USER_DIR = resolveUserDir();
const TRACKER_DIR = resolveDailiesDir(USER_DIR) || path.join(USER_DIR, 'pages/01.trips/italy-2026-demo/01.dailies');
/**
* The Grav route to the active dailies listing page,
* read from the post-form.md pageconfig.parent value.
* Falls back to '/trips/italy-2026-demo/dailies'.
* The Grav route to the active trip page, derived from the post-form.md
* pageconfig.parent value (the dailies container route, minus the trailing
* `/dailies`). Posted entries surface in this page's journal feed.
* Falls back to '/trips/italy-2026-demo'.
*/
function resolveActiveDailiesUrl() {
function resolveActiveTripUrl() {
const postFormPath = path.join(USER_DIR, 'pages/02.post/post-form.md');
if (!fs.existsSync(postFormPath)) return '/trips/italy-2026-demo/dailies';
if (!fs.existsSync(postFormPath)) return '/trips/italy-2026-demo';
const content = fs.readFileSync(postFormPath, 'utf-8');
const m = content.match(/parent:\s*['"]?(\/trips\/[^'"]+\/dailies)['"]?/);
return m ? m[1] : '/trips/italy-2026-demo/dailies';
const m = content.match(/parent:\s*['"]?(\/trips\/[^'"]+)\/dailies['"]?/);
return m ? m[1] : '/trips/italy-2026-demo';
}
const DAILIES_URL = resolveActiveDailiesUrl();
const ACTIVE_TRIP_URL = resolveActiveTripUrl();
/**
* Wait for all filepond items to finish XHR upload.
@@ -136,4 +137,4 @@ function readEntryMd(entryDir) {
return fs.readFileSync(path.join(entryDir, name), 'utf-8');
}
module.exports = { waitForFilePondUpload, postEntry, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR, DAILIES_URL };
module.exports = { waitForFilePondUpload, postEntry, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR, ACTIVE_TRIP_URL };
+14 -11
View File
@@ -48,11 +48,13 @@ test('MUX3: trip page map has a fullscreen toggle button', async ({ page }) => {
await expect(fsBtn).toHaveAttribute('aria-label', 'Expand map');
});
// ── MUX4: Dailies sort toggle reverses entry order ───────────────────────────
test('MUX4: dailies sort toggle reverses the feed entry order', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/dailies');
// ── MUX4: Trip feed sort toggle reverses entry order ─────────────────────────
// The sort toggle moved onto the trip page (#trip-sort-toggle) when the standalone
// /dailies feed view was retired.
test('MUX4: trip feed sort toggle reverses the feed entry order', async ({ page }) => {
await page.goto('/trips/italy-2026-demo');
const sortBtn = page.locator('#feed-sort-toggle');
const sortBtn = page.locator('#trip-sort-toggle');
await expect(sortBtn).toBeVisible();
const firstBefore = await page.locator('[data-type]').first().getAttribute('id');
@@ -67,21 +69,22 @@ test('MUX4: dailies sort toggle reverses the feed entry order', async ({ page })
expect(firstRestored, 'Entry order restored after second toggle').toBe(firstBefore);
});
// ── MUX5: Stories sort toggle reverses story card order ────────────────────
test('MUX5: stories sort toggle reverses the story card order', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/stories');
// ── MUX5: Trip feed sort toggle reverses story card order ────────────────────
// Story cards live in the trip feed as [data-type="story"] (the /stories grid was retired).
test('MUX5: trip feed sort toggle reverses the story card order', async ({ page }) => {
await page.goto('/trips/italy-2026-demo');
const sortBtn = page.locator('#feed-sort-toggle');
const sortBtn = page.locator('#trip-sort-toggle');
await expect(sortBtn).toBeVisible();
const firstBefore = await page.locator('.story-card').first().getAttribute('id');
const firstBefore = await page.locator('[data-type="story"]').first().getAttribute('id');
await sortBtn.click();
const firstAfter = await page.locator('.story-card').first().getAttribute('id');
const firstAfter = await page.locator('[data-type="story"]').first().getAttribute('id');
expect(firstAfter, 'Story order reversed after sort').not.toBe(firstBefore);
await sortBtn.click();
const firstRestored = await page.locator('.story-card').first().getAttribute('id');
const firstRestored = await page.locator('[data-type="story"]').first().getAttribute('id');
expect(firstRestored, 'Story order restored after second toggle').toBe(firstBefore);
});
+5 -104
View File
@@ -1,38 +1,11 @@
// @ts-check
// Tests: M1M4 — MapLibre GL canvas renders on all three map surfaces
// Tests: M4, M7, M8 — MapLibre GL renders on the two live map surfaces (home + trip page).
// The standalone /map, /dailies mini-map and /stories mini-map surfaces were retired
// (see docs/working/plans/2026-07-04-standalone-page-cleanup.md); their coverage now
// lives on the trip and home maps, both driven by MapUtils.initEntryMap().
// Requires demo data: run `make demo-load` before this suite.
const { test, expect } = require('@playwright/test');
// ── M1: Full map page renders MapLibre canvas ─────────────────────────────────
test('M1: /map page renders MapLibre GL canvas without JS errors', async ({ page }) => {
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto('/trips/italy-2026-demo/map');
await expect(page.locator('canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
expect(errors, 'No JS errors on map page').toHaveLength(0);
});
// ── M2: Full map page — dot markers are in the DOM ───────────────────────────
test('M2: /map page has at least one dot marker', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/map');
await expect(page.locator('canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
// Markers are added in map.on('load') — wait for first to appear in the DOM
await expect(page.locator('.maplibregl-marker').first()).toBeVisible({ timeout: 15000 });
const markerCount = await page.locator('.maplibregl-marker').count();
expect(markerCount, 'At least one marker present').toBeGreaterThan(0);
});
// ── M3: Dailies mini-map renders MapLibre canvas ─────────────────────────────
test('M3: Dailies mini-map renders MapLibre GL canvas without JS errors', async ({ page }) => {
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto('/trips/italy-2026-demo/dailies');
await expect(page.locator('#feed-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
expect(errors, 'No JS errors on dailies page').toHaveLength(0);
});
// ── M4: Home map renders MapLibre canvas ─────────────────────────────────────
test('M4: Home page map renders MapLibre GL canvas without JS errors', async ({ page }) => {
const errors = [];
@@ -43,42 +16,6 @@ test('M4: Home page map renders MapLibre GL canvas without JS errors', async ({
expect(errors, 'No JS errors on home page').toHaveLength(0);
});
// ── M5: Italy map — no JS errors with GPX present ────────────────────────────
test('M5: Italy map page renders without JS errors (GPX present)', async ({ page }) => {
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto('/trips/italy-2026-demo/map');
await expect(page.locator('canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
// Wait for markers to confirm map.on('load') completed
await expect(page.locator('.maplibregl-marker').first()).toBeVisible({ timeout: 15000 });
// Give Promise.all time to resolve
await page.waitForTimeout(3000);
expect(errors, 'No JS errors on Italy map page').toHaveLength(0);
});
// ── M6: Italy map — journey source exists after GPX loads ────────────────────
test('M6: Italy map has a journey MapLibre source after GPX settles', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/map');
await expect(page.locator('canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
await expect(page.locator('.maplibregl-marker').first()).toBeVisible({ timeout: 15000 });
// Wait until the journey source appears — addJourneySegments runs inside Promise.all.then()
// `var map = ...` in map.html.twig is a plain <script> var → available as window.map.
await page.waitForFunction(function () {
return window.map &&
(window.map.getSource('journey') !== undefined ||
window.map.getSource('journey-0') !== undefined);
}, { timeout: 15000 });
const hasSource = await page.evaluate(function () {
return !!(window.map.getSource('journey') || window.map.getSource('journey-0'));
});
expect(hasSource).toBe(true);
});
// ── M7: Clicking a trip-page map marker adds is-highlighted to the entry card ──
test('M7: clicking map marker briefly highlights the corresponding entry card', async ({ page }) => {
await page.goto('/trips/italy-2026-demo');
@@ -103,7 +40,7 @@ test('M7: clicking map marker briefly highlights the corresponding entry card',
// ── M8: Home map has GPX journey source on active trip ────────────────────────
test('M8: home map has a journey source after GPX settles (active trip)', async ({ page }) => {
// Requires travelling: true in user/config/site.yaml (set in Task 1).
// Requires travelling: true in user/config/site.yaml.
// Requires GPX files attached to the active trip (italy-2026-demo has 7).
const errors = [];
page.on('pageerror', e => errors.push(e.message));
@@ -125,39 +62,3 @@ test('M8: home map has a journey source after GPX settles (active trip)', async
expect(hasSource, 'Home map has a journey or GPX source').toBe(true);
expect(errors, 'No JS errors on home page').toHaveLength(0);
});
// ── M9: Stories mini-map renders MapLibre canvas ──────────────────────────────
test('M9: Stories mini-map renders MapLibre GL canvas without JS errors', async ({ page }) => {
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto('/trips/italy-2026-demo/stories');
await expect(page.locator('#stories-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
expect(errors, 'No JS errors on stories page').toHaveLength(0);
});
// ── M10: Stories mini-map has at least one story marker ──────────────────────
test('M10: Stories mini-map has at least one story marker', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/stories');
await expect(page.locator('#stories-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
await expect(page.locator('#stories-map .maplibregl-marker').first()).toBeVisible({ timeout: 15000 });
const markerCount = await page.locator('#stories-map .maplibregl-marker').count();
expect(markerCount, 'At least one story marker').toBeGreaterThan(0);
});
// ── M11: Dailies attribution control starts collapsed ─────────────────────────
test('M11: Dailies mini-map attribution starts collapsed (no open attribute)', async ({ page }) => {
await page.goto('/trips/italy-2026-demo/dailies');
await expect(page.locator('#feed-map canvas.maplibregl-canvas')).toBeVisible({ timeout: 10000 });
// Wait for markers (added in map.on('load')) to ensure the load callback has run,
// which is also where removeAttribute('open') executes.
await expect(page.locator('#feed-map .maplibregl-marker').first()).toBeVisible({ timeout: 15000 });
await expect(page.locator('#feed-map .maplibregl-ctrl-attrib')).toBeVisible({ timeout: 10000 });
const hasOpen = await page.evaluate(function () {
var attrib = document.querySelector('#feed-map .maplibregl-ctrl-attrib');
return attrib ? attrib.hasAttribute('open') : null;
});
expect(hasOpen, 'Attribution is collapsed (no open attribute)').toBe(false);
});
+10 -25
View File
@@ -1,47 +1,32 @@
// @ts-check
// Tests: N1N5 — page loads and navigation links
// Tests: N1N4 — page loads and navigation links.
// The standalone /map, /stats and /dailies views were retired; their content now
// lives on the trip page. N1/N2 are smoke tests for the two live landing surfaces.
const { test, expect } = require('@playwright/test');
// ── N1: /trips/italy-2026-demo/dailies renders ───────────────────────────────
test('N1: /trips/italy-2026-demo/dailies page loads with site header', async ({ page }) => {
// ── N1: Trip page renders with site header ───────────────────────────────────
test('N1: trip page loads with site header and title', async ({ page }) => {
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto('/trips/italy-2026-demo/dailies');
await page.goto('/trips/italy-2026-demo');
await expect(page.locator('.site-header')).toBeVisible();
await expect(page).toHaveTitle(/Into the East/i);
expect(errors).toHaveLength(0);
});
// ── N2: /trips/italy-2026-demo/map renders without JS errors ─────────────────
test('N2: /trips/italy-2026-demo/map page loads without JS errors', async ({ page }) => {
// ── N2: Home page renders without JS errors ──────────────────────────────────
test('N2: home page loads with site header and no JS errors', async ({ page }) => {
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto('/trips/italy-2026-demo/map');
await page.goto('/');
await expect(page.locator('.site-header')).toBeVisible();
expect(errors).toHaveLength(0);
});
// ── 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/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) ───────────────
// ── N4: trip page has Journal filter button (replaced the old sub-page nav link)
test('N4: trip page filter bar has Journal button', async ({ page }) => {
await page.goto('/trips/italy-2026-demo');
await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).toBeVisible();
});
// ── N5: "Map" nav link goes to /map ──────────────────────────────────────────
test.skip('N5: Map nav link navigates to /map', async ({ page }) => {
await page.goto('/');
await page.click('nav a[href*="map"]');
await expect(page).toHaveURL(/\/map/);
});
+5 -5
View File
@@ -4,7 +4,7 @@
const { test, expect } = require('@playwright/test');
const path = require('path');
const fs = require('fs');
const { waitForFilePondUpload, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR, DAILIES_URL } = require('../helpers');
const { waitForFilePondUpload, cleanupEntry, findEntry, readEntryMd, TRACKER_DIR, ACTIVE_TRIP_URL } = require('../helpers');
const TEST_PHOTO = path.join(__dirname, '../../fixtures/test-photo.jpg');
@@ -16,7 +16,7 @@ test.afterAll(() => {
});
// ── P1: Post without photo ─────────────────────────────────────────────────────
test('P1: post text-only entry → created on disk and visible on /dailies', async ({ page }) => {
test('P1: post text-only entry → created on disk and visible in trip feed', async ({ page }) => {
const tag = `p1-${Date.now()}`;
const title = `UI Test ${tag}`;
@@ -39,12 +39,12 @@ test('P1: post text-only entry → created on disk and visible on /dailies', asy
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
expect(photos.length, 'Text-only entry should have no photos').toBe(0);
await page.goto(DAILIES_URL);
await page.goto(ACTIVE_TRIP_URL);
await expect(page.locator('body')).toContainText(tag);
});
// ── P2: Post with photo ────────────────────────────────────────────────────────
test.skip('P2: post entry with photo → photo saved in entry folder and visible on /dailies', async ({ page }) => {
test.skip('P2: post entry with photo → photo saved in entry folder and visible in trip feed', async ({ page }) => {
const tag = `p2-${Date.now()}`;
const title = `UI Test ${tag}`;
@@ -70,7 +70,7 @@ test.skip('P2: post entry with photo → photo saved in entry folder and visible
const photos = fs.readdirSync(entryDir).filter(f => /\.(jpg|jpeg|png|webp|heic)$/i.test(f));
expect(photos.length, 'At least one photo should be saved').toBeGreaterThan(0);
await page.goto(DAILIES_URL);
await page.goto(ACTIVE_TRIP_URL);
await expect(page.locator('body')).toContainText(tag);
});
+13 -13
View File
@@ -3,15 +3,15 @@
// Requires demo data: run `make demo-load` before this suite.
const { test, expect } = require('@playwright/test');
const STORIES_URL = '/trips/italy-2026-demo/stories';
const TRIP_URL = '/trips/italy-2026-demo'; // stories now surface in the trip feed
const STORY_GALLERY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn'; // gallery-led: snap-gallery × 2, chapter-break, text-only pull-quote
const STORY_SCROLLY = '/trips/italy-2026-demo/stories/sorano-rock-and-time'; // scrolly-led: scrolly-section × 2, chapter-break, pull-quote with image
const DEMO_STORY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn'; // used for cross-trip hero sanity check
// ── S1: Stories listing shows cards ──────────────────────────────────────────
test('S1: stories listing renders at least 3 story cards', async ({ page }) => {
await page.goto(STORIES_URL);
const cards = page.locator('.story-card');
// ── S1: Trip feed shows story cards ──────────────────────────────────────────
test('S1: trip feed renders at least 3 story cards', async ({ page }) => {
await page.goto(TRIP_URL);
const cards = page.locator('[data-type="story"]');
await expect(cards.first()).toBeVisible({ timeout: 5000 });
const count = await cards.count();
expect(count, 'At least 3 story cards').toBeGreaterThanOrEqual(3);
@@ -55,16 +55,16 @@ test('S4: scrolly story page loads without JS errors', async ({ page }) => {
expect(errors, 'No JS errors on story page').toHaveLength(0);
});
// ── 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();
// ── S5: Back button returns to the originating trip page ─────────────────────
test('S5: story back button navigates back to the trip page', async ({ page }) => {
// Establish history: trip page → story → back
await page.goto(TRIP_URL);
await page.locator('[data-type="story"]').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-2026-demo\/stories$/);
await expect(page.locator('.story-card').first()).toBeVisible();
// story-escape runs history.back() when history exists → back to the trip page
await expect(page).toHaveURL(/\/trips\/italy-2026-demo$/);
await expect(page.locator('.journal-post').first()).toBeVisible();
});
// ── S6: Demo story — hero image sanity check ─────────────────────────────────