28 KiB
Accessibility Audit Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Status: 🔄 In progress — Task 1 complete (skip link), Tasks 2–6 open
Goal: Fix all eight WCAG 2.1 AA failures identified in the accessibility audit and add axe-core Playwright regression tests.
Architecture: Six sequential tasks — each implements one audit finding (or related group), writes a Playwright test first, then implements the fix in the relevant template/CSS/JS files. All tests go into a new tests/ui/accessibility.spec.js file that grows task by task. Task 6 adds axe-core automated scans on top of the feature-specific checks.
Tech Stack: Grav 2.0 Twig templates, CSS custom properties, vanilla JS, Playwright with @axe-core/playwright
Global Constraints
- Target standard: WCAG 2.1 Level AA
- Dev server: http://localhost:8081 (Docker container
intotheeast_grav) - Two git repos: outer at
/home/mischa/Nextcloud/Projects/travel-blog-intotheeast, user subrepo at/home/mischa/Projects/travel-blog-intotheeast/user - Template files are in the user subrepo (
user/themes/intotheeast/templates/,user/themes/intotheeast/css/) — commit there first, then commit the outer repo with the updateduser/pointer - Tests and
package.jsonare in the outer repo only - Run all Playwright tests with:
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast && npx playwright test --project=chromium - Demo data must be loaded before running tests:
make demo-load(run from outer repo) - Never read
.env; pass it only tomake/docker compose - Do NOT add comments to CSS or JS unless the WHY is non-obvious
Note on F8 (lightbox alt text): The existing JS in entry.html.twig already handles this correctly — open(index) sets lbImg.alt = btn.dataset.alt which is populated from {{ image.filename }}. No code change needed for F8.
Task 1: Skip link + <main id="main-content">
Fixes: F1 (WCAG 2.4.1 Bypass Blocks)
Files:
- Modify:
user/themes/intotheeast/templates/partials/base.html.twig - Modify:
user/themes/intotheeast/css/style.css - Create:
tests/ui/accessibility.spec.js
Interfaces:
-
Produces:
.skip-linkelement,#main-contentid on<main>— both consumed by A1 test -
Step 1: Create the failing test
Create tests/ui/accessibility.spec.js:
// @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();
});
- Step 2: Run A1 to verify it fails
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/accessibility.spec.js
Expected: FAIL — .skip-link not found.
- Step 3: Add skip link to
base.html.twig
In user/themes/intotheeast/templates/partials/base.html.twig:
Change line 15–16 (the opening <body> and <header>):
<body class="{% if page.template == 'map' %}map-page{% endif %}{% if page.template == 'home' or page.template == 'trip' %} home-page{% endif %}{% if page.template == 'story' %} template-story{% endif %}">
<header class="site-header">
Replace with:
<body class="{% if page.template == 'map' %}map-page{% endif %}{% if page.template == 'home' or page.template == 'trip' %} home-page{% endif %}{% if page.template == 'story' %} template-story{% endif %}">
<a class="skip-link" href="#main-content">Skip to main content</a>
<header class="site-header">
Then change line 25:
<main class="site-main">
Replace with:
<main class="site-main" id="main-content">
- Step 4: Add skip-link CSS to
style.css
In user/themes/intotheeast/css/style.css, find the :focus-visible rule (around line 782):
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
Add this block directly before it:
.skip-link {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus-visible {
left: 0;
top: 0;
width: auto;
height: auto;
overflow: visible;
padding: var(--space-2) var(--space-4);
background: var(--color-accent);
color: var(--color-accent-on);
font-family: var(--font-ui);
font-size: var(--text-sm);
font-weight: 600;
text-decoration: none;
z-index: 9999;
}
- Step 5: Run A1 to verify it passes
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/accessibility.spec.js
Expected: PASS.
- Step 6: Run existing tests to check for regressions
npx playwright test --project=chromium tests/ui/nav.spec.js tests/ui/home.spec.js tests/ui/dailies.spec.js
Expected: all pass.
- Step 7: Commit
cd /home/mischa/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/partials/base.html.twig themes/intotheeast/css/style.css
git commit -m "feat(a11y): add skip-to-main link and main landmark id"
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git add tests/ui/accessibility.spec.js user
git commit -m "test(a11y): add A1 skip link test"
Task 2: Color token contrast fixes
Fixes: F2 (--color-ink-muted fails 4.5:1), F3 (--color-accent fails 4.5:1)
Files:
- Modify:
user/themes/intotheeast/css/tokens.css - Modify:
tests/ui/accessibility.spec.js
Interfaces:
-
Consumes:
tests/ui/accessibility.spec.jsfrom Task 1 -
Produces: updated token values verified by A2 test
-
Step 1: Add the failing test
Append to tests/ui/accessibility.spec.js:
// ── 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');
});
- Step 2: Run A2 to verify it fails
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A2"
Expected: FAIL — values are #7a7268 and #2a8c73.
- Step 3: Update
tokens.css
In user/themes/intotheeast/css/tokens.css, make three changes:
Change --color-ink-muted:
--color-ink-muted: #7A7268; /* labels, timestamps, captions */
→
--color-ink-muted: #90887E; /* labels, timestamps, captions */
Change --color-accent:
--color-accent: #2A8C73; /* teal — lightened for dark contrast */
→
--color-accent: #2E9880; /* teal — lightened for dark contrast */
Change --color-accent-hover:
--color-accent-hover: #236655; /* hover/pressed teal */
→
--color-accent-hover: #287A68; /* hover/pressed teal */
Contrast verification (for reference only — these numbers are correct):
-
#90887Eon#1A1814= 5.07:1 ✓, on#22201B= 4.66:1 ✓ -
#2E9880on#1A1814= 5.00:1 ✓, on#22201B= 4.59:1 ✓ -
#287A68on#1A1814= 3.58:1 ✓ (non-text, needs 3:1) -
Step 4: Run A2 to verify it passes
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A2"
Expected: PASS.
- Step 5: Run all accessibility tests
npx playwright test --project=chromium tests/ui/accessibility.spec.js
Expected: A1 and A2 pass.
- Step 6: Commit
cd /home/mischa/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/css/tokens.css
git commit -m "feat(a11y): fix --color-ink-muted and --color-accent contrast ratios"
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git add tests/ui/accessibility.spec.js user
git commit -m "test(a11y): add A2 color contrast token test"
Task 3: ARIA states for filter buttons and toggle panels
Fixes: F4 (filter buttons lack aria-pressed), F5 (toggle buttons lack aria-expanded)
Files:
- Modify:
user/themes/intotheeast/templates/trip.html.twig - Modify:
tests/ui/accessibility.spec.js
Interfaces:
-
Consumes:
tests/ui/accessibility.spec.jsfrom Task 2 -
Produces:
aria-pressedon.trip-filter-btn,aria-expanded+aria-controlson#trip-stats-toggleand#trip-cycling-toggle -
Step 1: Add the failing tests
Append to tests/ui/accessibility.spec.js:
// ── 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');
});
- Step 2: Run A3a–A3d to verify they fail
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A3"
Expected: all four FAIL — attributes not present.
- Step 3: Add
aria-pressedto filter buttons in template
In user/themes/intotheeast/templates/trip.html.twig, find lines 124–128:
<div class="trip-filter-group">
<button class="trip-filter-btn is-active" data-filter="all">All content</button>
<button class="trip-filter-btn" data-filter="journal">Journal</button>
<button class="trip-filter-btn" data-filter="story">Stories</button>
</div>
Replace with:
<div class="trip-filter-group">
<button class="trip-filter-btn is-active" data-filter="all" aria-pressed="true">All content</button>
<button class="trip-filter-btn" data-filter="journal" aria-pressed="false">Journal</button>
<button class="trip-filter-btn" data-filter="story" aria-pressed="false">Stories</button>
</div>
- Step 4: Add
aria-expanded+aria-controlsto toggle buttons in template
Find lines 129–134:
<div class="trip-filter-group">
<button class="trip-stats-btn" id="trip-stats-toggle">Stats</button>
{% if has_gpx %}
<button class="trip-stats-btn" id="trip-cycling-toggle">Cycling</button>
{% endif %}
</div>
Replace with:
<div class="trip-filter-group">
<button class="trip-stats-btn" id="trip-stats-toggle" aria-expanded="false" aria-controls="trip-stats-block">Stats</button>
{% if has_gpx %}
<button class="trip-stats-btn" id="trip-cycling-toggle" aria-expanded="false" aria-controls="trip-cycling-block">Cycling</button>
{% endif %}
</div>
- Step 5: Update the filter JS to toggle
aria-pressed
In the same file, find the filter click handler inside the <script> block (around line 384):
filterBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
filterBtns.forEach(function(b) { b.classList.remove('is-active'); });
btn.classList.add('is-active');
Replace those four lines with:
filterBtns.forEach(function(btn) {
btn.addEventListener('click', function() {
filterBtns.forEach(function(b) { b.classList.remove('is-active'); b.setAttribute('aria-pressed', 'false'); });
btn.classList.add('is-active');
btn.setAttribute('aria-pressed', 'true');
- Step 6: Update the stats toggle JS to set
aria-expanded
Find the stats toggle handler (around line 548):
statsToggle.addEventListener('click', function() {
var isOpen = statsBlock.style.display !== 'none';
statsBlock.style.display = isOpen ? 'none' : '';
statsToggle.classList.toggle('is-active', !isOpen);
});
Replace with:
statsToggle.addEventListener('click', function() {
var isOpen = statsBlock.style.display !== 'none';
statsBlock.style.display = isOpen ? 'none' : '';
statsToggle.classList.toggle('is-active', !isOpen);
statsToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
- Step 7: Update the cycling toggle JS to set
aria-expanded
Find the cycling toggle handler (around line 560):
cycToggle.addEventListener('click', function() {
var isOpen = cycBlock.style.display !== 'none';
cycBlock.style.display = isOpen ? 'none' : '';
cycToggle.classList.toggle('is-active', !isOpen);
});
Replace with:
cycToggle.addEventListener('click', function() {
var isOpen = cycBlock.style.display !== 'none';
cycBlock.style.display = isOpen ? 'none' : '';
cycToggle.classList.toggle('is-active', !isOpen);
cycToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
});
- Step 8: Run A3a–A3d to verify they pass
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A3"
Expected: all four PASS.
- Step 9: Run the existing filter tests to check for regressions
npx playwright test --project=chromium tests/ui/trip-filter.spec.js
Expected: all pass.
- Step 10: Commit
cd /home/mischa/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/trip.html.twig
git commit -m "feat(a11y): add aria-pressed to filter buttons and aria-expanded to stats/cycling toggles"
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git add tests/ui/accessibility.spec.js user
git commit -m "test(a11y): add A3a-A3d aria-pressed and aria-expanded tests"
Task 4: Photo strip keyboard navigation
Fixes: F6 (photo strip not keyboard-navigable)
Files:
- Modify:
user/themes/intotheeast/templates/partials/base.html.twig - Modify:
user/themes/intotheeast/css/style.css - Modify:
tests/ui/accessibility.spec.js
Interfaces:
-
Consumes:
tests/ui/accessibility.spec.jsfrom Task 3 -
Produces:
.strip-controlsdiv with.strip-prev/.strip-nextbuttons injected by JS for strips withdata-slides >= 2; all strips getrole="region"andaria-label="Photo strip" -
Step 1: Add the failing tests
Append to tests/ui/accessibility.spec.js:
// ── 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');
});
- Step 2: Run A4a–A4b to verify they fail
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A4"
Expected: A4a FAIL (no role attribute), A4b PASS vacuously (no multi-slide strips in demo data).
- Step 3: Replace the dot-sync IIFE in
base.html.twig
In user/themes/intotheeast/templates/partials/base.html.twig, find the IIFE (lines 30–41):
<script>
(function () {
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
var dots = strip.nextElementSibling;
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
strip.addEventListener('scroll', function () {
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
}, { passive: true });
});
})();
</script>
Replace with:
<script>
(function () {
document.querySelectorAll('.journal-photo-strip').forEach(function (strip) {
strip.setAttribute('role', 'region');
strip.setAttribute('aria-label', 'Photo strip');
var slideCount = parseInt(strip.dataset.slides, 10) || 1;
var dots = strip.nextElementSibling;
if (!dots || !dots.classList.contains('journal-photo-dots')) return;
var dotEls = Array.from(dots.querySelectorAll('.journal-photo-dot'));
strip.addEventListener('scroll', function () {
var idx = Math.round(strip.scrollLeft / strip.offsetWidth);
dotEls.forEach(function (d, i) { d.classList.toggle('is-active', i === idx); });
}, { passive: true });
if (slideCount < 2) return;
var prev = document.createElement('button');
prev.className = 'strip-prev';
prev.setAttribute('aria-label', 'Previous photo');
prev.textContent = '‹';
prev.addEventListener('click', function () {
strip.scrollBy({ left: -strip.offsetWidth, behavior: 'smooth' });
});
var next = document.createElement('button');
next.className = 'strip-next';
next.setAttribute('aria-label', 'Next photo');
next.textContent = '›';
next.addEventListener('click', function () {
strip.scrollBy({ left: strip.offsetWidth, behavior: 'smooth' });
});
var controls = document.createElement('div');
controls.className = 'strip-controls';
controls.appendChild(prev);
controls.appendChild(next);
dots.insertAdjacentElement('afterend', controls);
});
})();
</script>
- Step 4: Add strip-controls CSS to
style.css
In user/themes/intotheeast/css/style.css, find the .journal-photo-dot.is-active rule (around line 245):
.journal-photo-dot.is-active {
background: var(--color-ink-muted);
}
Add this block directly after it:
.strip-controls {
display: flex;
justify-content: center;
gap: var(--space-3);
margin-top: calc(-1 * var(--space-2));
margin-bottom: var(--space-4);
}
.strip-prev,
.strip-next {
background: transparent;
border: 1px solid var(--color-border);
color: var(--color-ink-2);
border-radius: var(--radius-sm);
padding: var(--space-1) var(--space-3);
font-size: var(--text-md);
line-height: 1;
cursor: pointer;
}
.strip-prev:hover,
.strip-next:hover {
border-color: var(--color-accent);
color: var(--color-accent);
}
- Step 5: Run A4a–A4b to verify they pass
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A4"
Expected: both PASS.
- Step 6: Run the full accessibility suite
npx playwright test --project=chromium tests/ui/accessibility.spec.js
Expected: A1–A4 all pass.
- Step 7: Commit
cd /home/mischa/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/partials/base.html.twig themes/intotheeast/css/style.css
git commit -m "feat(a11y): add keyboard prev/next to photo strip and region landmark"
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git add tests/ui/accessibility.spec.js user
git commit -m "test(a11y): add A4a-A4b photo strip keyboard tests"
Task 5: GPX delete button unique accessible names
Fixes: F7 (delete buttons have no unique name)
Files:
- Modify:
user/themes/intotheeast/templates/gpx-manager.html.twig - Modify:
tests/ui/accessibility.spec.js
Interfaces:
-
Consumes:
tests/ui/accessibility.spec.jsfrom Task 4 -
Produces: delete buttons with
aria-label="Delete <filename>" -
Step 1: Add the failing test
Append to tests/ui/accessibility.spec.js:
// ── 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-delete[data-filename="tokyo-day1.gpx"]');
await expect(deleteBtn).toBeVisible();
await expect(deleteBtn).toHaveAttribute('aria-label', 'Delete tokyo-day1.gpx');
});
- Step 2: Run A5 to verify it fails
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A5"
Expected: FAIL — aria-label attribute not present on the delete button.
- Step 3: Add
aria-labelto the delete button ingpx-manager.html.twig
In user/themes/intotheeast/templates/gpx-manager.html.twig, find line 99 inside the rows template string:
<td><button class="gpx-delete" data-filename="${f.filename}">Delete</button></td>
Replace with:
<td><button class="gpx-delete" data-filename="${f.filename}" aria-label="Delete ${f.filename}">Delete</button></td>
- Step 4: Run A5 to verify it passes
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "A5"
Expected: PASS.
- Step 5: Run full accessibility suite
npx playwright test --project=chromium tests/ui/accessibility.spec.js
Expected: A1–A5 all pass.
- Step 6: Commit
cd /home/mischa/Projects/travel-blog-intotheeast/user
git add themes/intotheeast/templates/gpx-manager.html.twig
git commit -m "feat(a11y): add unique aria-label to GPX delete buttons"
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git add tests/ui/accessibility.spec.js user
git commit -m "test(a11y): add A5 GPX delete button accessible name test"
Task 6: axe-core WCAG 2.1 AA regression scans
Adds: AX1–AX5 automated axe scans across all main page types
Files:
- Modify:
package.json - Modify:
tests/ui/accessibility.spec.js
Interfaces:
-
Consumes:
tests/ui/accessibility.spec.jsfrom Task 5 -
Produces: five axe scans that fail on any
criticalorseriousWCAG violation -
Step 1: Install
@axe-core/playwright
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npm install --save-dev @axe-core/playwright
After install, package.json devDependencies should include "@axe-core/playwright": "^4.x.x" (exact semver will vary).
- Step 2: Add the axe scans to
accessibility.spec.js
Append to tests/ui/accessibility.spec.js:
// ── 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/japan-korea-2026');
axeScan('AX3', '/trips/japan-korea-2026/dailies');
axeScan('AX4', '/trips/japan-korea-2026/dailies/2026-03-25-1540-wheels-down-narita');
axeScan('AX5', '/trips');
- Step 3: Run the axe scans
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
npx playwright test --project=chromium tests/ui/accessibility.spec.js --grep "AX"
Expected: all five PASS (after Tasks 1–5 fixed the known violations). If any fail, read the violation output — it will name the rule ID, description, and offending HTML. Fix the violation if it represents a real issue, or note it in the ledger if it is a known limitation outside scope (e.g. map canvas element).
- Step 4: Run the full accessibility test suite
npx playwright test --project=chromium tests/ui/accessibility.spec.js
Expected: A1–A5 and AX1–AX5 all pass (10 tests total).
- Step 5: Run the full Playwright suite to check for regressions
npx playwright test --project=chromium
Expected: all tests pass (or pre-existing failures only — check the progress ledger for known pre-existing failures before marking as blocker).
- Step 6: Commit
cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast
git add package.json package-lock.json tests/ui/accessibility.spec.js
git commit -m "test(a11y): add axe-core WCAG 2.1 AA regression scans AX1-AX5"