// @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/japan-korea-2026'; 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'); }); // ── A4: Photo strip keyboard navigation ─────────────────────────────────────── test('A4a: all photo strips have role=region and aria-label', async ({ page }) => { await page.goto('/trips/japan-korea-2026/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/japan-korea-2026/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'); });