From 8c32ac707ef8aab1b6ca6a5b6e94c277b6725d32 Mon Sep 17 00:00:00 2001 From: Mischa Date: Sun, 21 Jun 2026 17:16:57 +0200 Subject: [PATCH] =?UTF-8?q?test:=20merge=20playwright-tests=20branch=20?= =?UTF-8?q?=E2=80=94=20reorganise,=20extend,=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM --- tests/ui/accessibility.spec.js | 140 ------------------------------- tests/ui/home-highlights.spec.js | 57 ------------- tests/ui/home.spec.js | 10 --- 3 files changed, 207 deletions(-) delete mode 100644 tests/ui/accessibility.spec.js delete mode 100644 tests/ui/home-highlights.spec.js delete mode 100644 tests/ui/home.spec.js diff --git a/tests/ui/accessibility.spec.js b/tests/ui/accessibility.spec.js deleted file mode 100644 index ff954f0..0000000 --- a/tests/ui/accessibility.spec.js +++ /dev/null @@ -1,140 +0,0 @@ -// @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'); diff --git a/tests/ui/home-highlights.spec.js b/tests/ui/home-highlights.spec.js deleted file mode 100644 index d1624df..0000000 --- a/tests/ui/home-highlights.spec.js +++ /dev/null @@ -1,57 +0,0 @@ -// @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); - // 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/); - }); -}); diff --git a/tests/ui/home.spec.js b/tests/ui/home.spec.js deleted file mode 100644 index ce1dde2..0000000 --- a/tests/ui/home.spec.js +++ /dev/null @@ -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(); -});