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
|
// @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');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
// Known fixture entries that always exist in the repo
|
// Known fixture entries that always exist in the repo
|
||||||
const KNOWN_SLUG = '2026-03-25-1540-wheels-down-narita.entry';
|
const KNOWN_SLUG = '2026-09-01-0700-setting-off-from-campiglia.entry';
|
||||||
const KNOWN_TITLE = 'Wheels Down at Narita';
|
const KNOWN_TITLE = 'Setting Off from Campiglia';
|
||||||
const KNOWN_CITY = 'Tokyo';
|
const KNOWN_CITY = 'Campiglia Marittima';
|
||||||
const KNOWN_COUNTRY = 'Japan';
|
const KNOWN_COUNTRY = 'Italy';
|
||||||
|
|
||||||
// Use two fixture entries with different dates to verify descending order
|
// Use two real entries from central-asia-2023 to verify descending order
|
||||||
const NEWER_SLUG = '2026-06-17.entry'; // most recent fixture (June 17)
|
const NEWER_SLUG = '2023-10-18-hunting-the-mother-of-georgia-from-above.entry'; // newest date in that trip
|
||||||
const OLDER_SLUG = '2026-03-25-1540-wheels-down-narita.entry'; // oldest fixture (March 25)
|
const OLDER_SLUG = '2023-08-28-welcome-to-my-central-asian-picture-diary.entry'; // oldest date in that trip
|
||||||
|
|
||||||
// ── T1: Dailies page loads ─────────────────────────────────────────────────────
|
// ── T1: Dailies page loads ─────────────────────────────────────────────────────
|
||||||
test('T1: /trips/japan-korea-2026/dailies loads and shows at least one entry card', async ({ page }) => {
|
test('T1: /trips/italy-2026-demo/dailies loads and shows at least one entry card', async ({ page }) => {
|
||||||
await page.goto('/trips/japan-korea-2026/dailies');
|
await page.goto('/trips/italy-2026-demo/dailies');
|
||||||
await expect(page.locator('.entry-card').first()).toBeVisible();
|
await expect(page.locator('.journal-post').first()).toBeVisible();
|
||||||
await expect(page.locator('.site-header')).toBeVisible();
|
await expect(page.locator('.site-header')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── T2: Entries are newest-first ──────────────────────────────────────────────
|
// ── T2: Entries are newest-first ──────────────────────────────────────────────
|
||||||
// Verify using two known fixture entries rather than all entries
|
// Verify using two known real entries from central-asia-2023 (22 entries, stable order).
|
||||||
// (the dailies may contain noisy test-run debris with inconsistent dates).
|
|
||||||
test('T2: dailies shows newer entries before older entries', async ({ page }) => {
|
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
|
// Use attribute selector to handle dots in slug names (CSS dots are class selectors)
|
||||||
const newerCard = page.locator(`.entry-card a[href*="${NEWER_SLUG}"]`);
|
const newerCard = page.locator(`.journal-post[id="entry-${NEWER_SLUG}"]`);
|
||||||
const olderCard = page.locator(`.entry-card a[href*="${OLDER_SLUG}"]`);
|
const olderCard = page.locator(`.journal-post[id="entry-${OLDER_SLUG}"]`);
|
||||||
|
|
||||||
await expect(newerCard).toBeVisible();
|
await expect(newerCard).toBeVisible();
|
||||||
await expect(olderCard).toBeVisible();
|
await expect(olderCard).toBeVisible();
|
||||||
|
|
||||||
// The newer entry should appear higher in the DOM (lower index)
|
// The newer entry should appear higher in the DOM (lower index)
|
||||||
const newerIdx = await newerCard.evaluate(el => {
|
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 => {
|
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);
|
expect(newerIdx).toBeLessThan(olderIdx);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── T3: Individual entry page loads ───────────────────────────────────────────
|
// ── T3: Individual entry page loads ───────────────────────────────────────────
|
||||||
test('T3: individual entry page loads at /trips/japan-korea-2026/dailies/{slug}', async ({ page }) => {
|
test('T3: individual entry page loads at /trips/italy-2026-demo/dailies/{slug}', 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('article.entry')).toBeVisible();
|
await expect(page.locator('article.entry')).toBeVisible();
|
||||||
await expect(page.locator('.site-header')).toBeVisible();
|
await expect(page.locator('.site-header')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── T4: Entry page shows title, date, and content ─────────────────────────────
|
// ── T4: Entry page shows title, date, and content ─────────────────────────────
|
||||||
test('T4: entry page shows title and body content', async ({ page }) => {
|
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-title')).toContainText(KNOWN_TITLE);
|
||||||
await expect(page.locator('.entry-body')).not.toBeEmpty();
|
await expect(page.locator('.entry-body')).not.toBeEmpty();
|
||||||
await expect(page.locator('time.entry-date')).toBeVisible();
|
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 ────────────────────────────────
|
// ── T5: Entry page shows location when present ────────────────────────────────
|
||||||
test('T5: entry page shows city and country when set', async ({ page }) => {
|
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_CITY);
|
||||||
await expect(page.locator('.entry-location')).toContainText(KNOWN_COUNTRY);
|
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
|
// @ts-check
|
||||||
// Tests: G1–G5 — buildJourneySegments algorithm correctness
|
// 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.
|
// then call the functions with synthetic data via page.evaluate.
|
||||||
// Requires demo data: run `make demo-load` before this suite.
|
// Requires demo data: run `make demo-load` before this suite.
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
async function getMapUtils(page) {
|
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 });
|
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: '44.0', lng: '12.0', force_connect: false },
|
||||||
{ lat: '45.0', lng: '13.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);
|
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 };
|
var e2 = { lat: '43.010', lng: '11.010', force_connect: false };
|
||||||
// Trackpoints covering both (stored as [lat, lng])
|
// Trackpoints covering both (stored as [lat, lng])
|
||||||
var track = [[43.000, 11.000], [43.005, 11.005], [43.010, 11.010]];
|
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);
|
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 e1 = { lat: '43.000', lng: '11.000', force_connect: false };
|
||||||
var e2 = { lat: '43.010', lng: '11.010', force_connect: true };
|
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]];
|
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);
|
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
|
// Two separate files — each only covers one marker
|
||||||
var trackA = [[43.000, 11.000], [43.005, 11.005]]; // near e1 only
|
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
|
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);
|
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 e2 = { lat: '43.010', lng: '11.010', force_connect: false };
|
||||||
var e3 = { lat: '45.000', lng: '13.000', 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 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;
|
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
|
// Tests: N1–N5 — page loads and navigation links
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
// ── N1: /trips/japan-korea-2026/dailies renders ───────────────────────────────
|
// ── N1: /trips/italy-2026-demo/dailies renders ───────────────────────────────
|
||||||
test('N1: /trips/japan-korea-2026/dailies page loads with site header', async ({ page }) => {
|
test('N1: /trips/italy-2026-demo/dailies page loads with site header', async ({ page }) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
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.locator('.site-header')).toBeVisible();
|
||||||
await expect(page).toHaveTitle(/Into the East/i);
|
await expect(page).toHaveTitle(/Into the East/i);
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── N2: /trips/japan-korea-2026/map renders without JS errors ─────────────────
|
// ── N2: /trips/italy-2026-demo/map renders without JS errors ─────────────────
|
||||||
test('N2: /trips/japan-korea-2026/map page loads without JS errors', async ({ page }) => {
|
test('N2: /trips/italy-2026-demo/map page loads without JS errors', async ({ page }) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
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();
|
await expect(page.locator('.site-header')).toBeVisible();
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── N3: /trips/japan-korea-2026/stats renders ─────────────────────────────────
|
// ── N3: /trips/italy-2026-demo/stats renders ─────────────────────────────────
|
||||||
test('N3: /trips/japan-korea-2026/stats page loads with site header', async ({ page }) => {
|
test('N3: /trips/italy-2026-demo/stats page loads with site header', async ({ page }) => {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
page.on('pageerror', e => errors.push(e.message));
|
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();
|
await expect(page.locator('.site-header')).toBeVisible();
|
||||||
expect(errors).toHaveLength(0);
|
expect(errors).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── N4: trip page has Journal filter button (replaced nav link) ───────────────
|
// ── N4: trip page has Journal filter button (replaced nav link) ───────────────
|
||||||
test('N4: trip page filter bar has Journal button', async ({ page }) => {
|
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();
|
await expect(page.locator('.trip-filter-btn[data-filter="journal"]')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+20
-17
@@ -1,12 +1,12 @@
|
|||||||
// @ts-check
|
// @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.
|
// Requires demo data: run `make demo-load` before this suite.
|
||||||
const { test, expect } = require('@playwright/test');
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
const STORIES_URL = '/trips/italy-2025/stories';
|
const STORIES_URL = '/trips/italy-2026-demo/stories';
|
||||||
const STORY_GALLERY = '/trips/italy-2025/stories/val-dorcia-dawn'; // gallery-led: snap-gallery × 2, chapter-break, text-only pull-quote
|
const STORY_GALLERY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn';
|
||||||
const STORY_SCROLLY = '/trips/italy-2025/stories/long-climb-montalcino'; // scrolly-led: scrolly-section × 2, chapter-break, pull-quote with image
|
const STORY_SCROLLY = '/trips/italy-2026-demo/stories/sorano-rock-and-time';
|
||||||
const JAPAN_STORY = '/trips/japan-korea-2026/stories/the-thousand-gates';
|
const DEMO_STORY = '/trips/italy-2026-demo/stories/val-dorcia-at-dawn';
|
||||||
|
|
||||||
// ── S1: Stories listing shows cards ──────────────────────────────────────────
|
// ── S1: Stories listing shows cards ──────────────────────────────────────────
|
||||||
test('S1: stories listing renders at least 3 story cards', async ({ page }) => {
|
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 ──
|
// ── 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 }) => {
|
test('S2: gallery-led story renders hero, snap-gallery, chapter-break, text-only pull-quote', async ({ page }) => {
|
||||||
await page.goto(STORY_GALLERY);
|
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')).toBeVisible({ timeout: 8000 });
|
||||||
await expect(page.locator('.story-hero__img-placeholder')).toHaveCount(0);
|
await expect(page.locator('.story-hero__img-placeholder')).toHaveCount(0);
|
||||||
// Snap-gallery (there are two in this story)
|
|
||||||
const galleries = page.locator('.pgallery');
|
const galleries = page.locator('.pgallery');
|
||||||
await expect(galleries.first()).toBeVisible();
|
await expect(galleries.first()).toBeVisible();
|
||||||
expect(await galleries.count(), 'Two snap-galleries').toBe(2);
|
expect(await galleries.count(), 'Two snap-galleries').toBe(2);
|
||||||
// Chapter-break
|
|
||||||
await expect(page.locator('.chapter-break')).toBeVisible();
|
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();
|
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 }) => {
|
test('S3: scrolly-led story renders two scrolly-sections and pull-quote with background image', async ({ page }) => {
|
||||||
await page.goto(STORY_SCROLLY);
|
await page.goto(STORY_SCROLLY);
|
||||||
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
|
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
|
||||||
// Two scrolly-sections
|
|
||||||
const scrollySections = page.locator('.scrolly');
|
const scrollySections = page.locator('.scrolly');
|
||||||
await expect(scrollySections.first()).toBeVisible();
|
await expect(scrollySections.first()).toBeVisible();
|
||||||
expect(await scrollySections.count(), 'Two scrolly-sections').toBe(2);
|
expect(await scrollySections.count(), 'Two scrolly-sections').toBe(2);
|
||||||
// Pull-quote with background image
|
|
||||||
await expect(page.locator('.pull-quote__bg')).toBeVisible();
|
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 ────────────────────────────────
|
// ── S5: Back button returns to stories listing ────────────────────────────────
|
||||||
test('S5: back button navigates back to stories listing', async ({ page }) => {
|
test('S5: back button navigates back to stories listing', async ({ page }) => {
|
||||||
// Establish history: listing → story → back
|
|
||||||
await page.goto(STORIES_URL);
|
await page.goto(STORIES_URL);
|
||||||
await page.locator('.story-card').first().click();
|
await page.locator('.story-card').first().click();
|
||||||
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
|
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
|
||||||
await page.locator('.story-escape').click();
|
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).toHaveURL(/italy-2025\/stories$/);
|
|
||||||
await expect(page.locator('.story-card').first()).toBeVisible();
|
await expect(page.locator('.story-card').first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── S6: Japan story — cross-trip hero image sanity check ─────────────────────
|
// ── S6: Demo story — hero image sanity check ─────────────────────────────────
|
||||||
test('S6: Japan story renders hero image without placeholder', async ({ page }) => {
|
test('S6: demo story renders hero image without placeholder', async ({ page }) => {
|
||||||
await page.goto(JAPAN_STORY);
|
await page.goto(DEMO_STORY);
|
||||||
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
|
await expect(page.locator('.story-hero__img')).toBeVisible({ timeout: 8000 });
|
||||||
await expect(page.locator('.story-hero__img-placeholder')).toHaveCount(0);
|
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