test: fix stale trip-slug references; add home, highlights, a11y specs
This commit is contained in:
@@ -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');
|
||||
+35
-23
@@ -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/);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
+20
-17
@@ -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/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user