test: merge playwright-tests branch — reorganise, extend, fix

- Reorganise flat tests/ui/ into feature subdirectories (auth/ post/ gpx/
  maps/ stories/ dailies/ home/ nav/ trip/ a11y/)
- Fix stale trip-slug refs (japan-korea-2026 / italy-2025 → italy-2026-demo)
- Add tests/fixtures/test-route.gpx
- Add GPX Manager spec (GM1–GM7) with real API calls and afterAll cleanup
- Add post-form tests P6–P8 (success message, date frontmatter, form reset)
- Add AX6 (gpx-manager mocked) and AX7 (story page) axe scans
- Fix auth.setup.js AUTH_FILE path; add @axe-core/playwright devDependency
- Fix fixture paths in post specs after subdirectory move

Known failures (not regressions):
  AX6/AX7 — real a11y violations in site (color-contrast, label, scrollable)
  H1/M8   — require travelling:true; site is currently between trips

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
This commit is contained in:
2026-06-21 17:16:57 +02:00
parent db7c102da1
commit 8c32ac707e
3 changed files with 0 additions and 207 deletions
-140
View File
@@ -1,140 +0,0 @@
// @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');
-57
View File
@@ -1,57 +0,0 @@
// @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);
// Brief pause for Grav to re-read config on next request
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('/');
// Requires at least one featured demo entry with lat/lng set (see demo seed in user/docs/demo/)
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
@@ -1,10 +0,0 @@
// @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();
});