From 569dbe6107b231d22e71c0defee042bb07d841e6 Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 09:34:34 +0100 Subject: [PATCH 1/8] feat: add a new skill for accessibility debugging and auditing with Chrome DevTools MCP. --- skills/a11y-debugging/SKILL.md | 127 +++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 skills/a11y-debugging/SKILL.md diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md new file mode 100644 index 000000000..ef4f67f24 --- /dev/null +++ b/skills/a11y-debugging/SKILL.md @@ -0,0 +1,127 @@ +--- +name: a11y-debugging +description: Uses Chrome DevTools MCP for accessibility (a11y) debugging and auditing based on web.dev guidelines. Use when testing semantic HTML, ARIA labels, focus states, keyboard navigation, tap targets, and color contrast. +--- + +## Core Concepts + +**Accessibility Tree vs DOM**: Visually hiding an element (e.g., `CSS opacity: 0`) behaves differently for screen readers than `display: none` or `aria-hidden="true"`. The `take_snapshot` tool returns the accessibility tree of the page, which represents what assistive technologies "see", making it the most reliable source of truth for semantic structure. + +**Reading web.dev documentation**: If you need to research specific accessibility guidelines (like `https://web.dev/articles/accessible-tap-targets`), you can append `.md.txt` to the URL (e.g., `https://web.dev/articles/accessible-tap-targets.md.txt`) to fetch the clean, raw markdown version. This is much easier to read using the `read_url_content` tool! + +## Workflow Patterns + +### 1. Browser Issues & Audits + +Chrome automatically checks for common accessibility problems. Use `list_console_messages` to check for these native audits first: +- `types`: `["issue"]` +- `includePreservedMessages`: `true` (to catch issues that occurred during page load) + +This often reveals missing labels, invalid ARIA attributes, and other critical errors without manual investigation. + +### 2. Semantics & Structure + +The accessibility tree exposes the heading hierarchy and semantic landmarks. + +1. Navigate to the page. +2. Use `take_snapshot` to capture the accessibility tree. +3. **Check Heading Levels**: Ensure heading levels (`h1`, `h2`, `h3`, etc.) are logical and do not skip levels. The snapshot will include heading roles. +4. **Content Reordering**: Verify that the DOM order (which drives the accessibility tree) matches the visual reading order. Use `take_screenshot` to inspect the visual layout and compare it against the snapshot structure to catch CSS floats or absolute positioning that jumbles the logical flow. + +### 3. Labels, Forms & Text Alternatives + +1. Locate buttons, inputs, and images in the `take_snapshot` output. +2. Ensure interactive elements have an accessible name (e.g., a button should not just say `""` if it only contains an icon). +3. **Orphaned Inputs**: Verify that all form inputs have associated labels. Use `evaluate_script` to check for inputs missing `id` (for `label[for]`) or `aria-label`: + ```javascript + () => { + const inputs = Array.from(document.querySelectorAll('input, select, textarea')); + return inputs.filter(i => { + const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); + const hasAria = i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); + const hasImplicitLabel = i.closest('label'); + return !hasId && !hasAria && !hasImplicitLabel; + }).map(i => ({ tag: i.tagName, id: i.id, name: i.name, placeholder: i.placeholder })); + } + ``` +4. Check images for `alt` text. + +### 4. Focus & Keyboard Navigation + +Testing "keyboard traps" and proper focus management without visual feedback relies on precise scripting. + +1. Use `evaluate_script` to find the currently focused element: + ```javascript + () => { + const active = document.activeElement; + return { tag: active.tagName, id: active.id, className: active.className, text: active.innerText }; + } + ``` +2. Use the `press_key` tool with `"Tab"` or `"Shift+Tab"` to move focus. +3. Re-run the script in step 1 to ensure focus moved to the expected next interactive element. +4. If a modal opens, focus must move into the modal and "trap" within it until closed. + +### 5. Tap Targets and Visuals + +According to web.dev, tap targets should be at least 48x48 pixels with sufficient spacing. Since the accessibility tree doesn't show sizes, use `evaluate_script`: + +```javascript +(el) => { + const rect = el.getBoundingClientRect(); + return { width: rect.width, height: rect.height }; +} +``` +*Pass the element's `uid` from the snapshot as an argument to the tool.* + +### 6. Color Contrast + +To verify color contrast ratios without the DevTools UI, use `evaluate_script` to compute the relative luminance of the text (`color`) and background (`backgroundColor`). + +```javascript +(el) => { + function getRGB(colorStr) { + const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + return match ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] : [255, 255, 255]; + } + function luminance(r, g, b) { + const a = [r, g, b].map(function (v) { + v /= 255; + return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }); + return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; + } + + const style = window.getComputedStyle(el); + const fg = getRGB(style.color); + let bg = getRGB(style.backgroundColor); + + // Basic contrast calculation (Note: Doesn't account for transparency over background images) + const l1 = luminance(fg[0], fg[1], fg[2]); + const l2 = luminance(bg[0], bg[1], bg[2]); + const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); + + return { color: style.color, bg: style.backgroundColor, contrastRatio: ratio.toFixed(2) }; +} +``` +*Pass the element's `uid` to test the contrast against WCAG AA (4.5:1 for normal text, 3:1 for large text).* + +### 7. Global Page Checks + +Verify document-level accessibility settings often missed in component testing: + +```javascript +() => { + return { + lang: document.documentElement.lang || 'MISSING - Screen readers need this for pronunciation', + title: document.title || 'MISSING - Required for context', + viewport: document.querySelector('meta[name="viewport"]')?.content || 'MISSING - Check for user-scalable=no (bad practice)', + reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'Enabled' : 'Disabled' + }; +} +``` + +## Troubleshooting + +If standard a11y queries fail or the `evaluate_script` snippets return unexpected results: + +* **Visual Inspection**: If automated scripts cannot determine contrast (e.g., text over gradient images or complex backgrounds), use `take_screenshot` to capture the element. While models cannot measure exact contrast ratios from images, they can visually assess legibility and identifying obvious issues. From 9a73b606279c2ef37463bdcf5c30dcb37f04ffbd Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 09:36:09 +0100 Subject: [PATCH 2/8] Reformat --- skills/a11y-debugging/SKILL.md | 90 ++++++++++++++++++++++------------ 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index ef4f67f24..36021f6b0 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -14,8 +14,9 @@ description: Uses Chrome DevTools MCP for accessibility (a11y) debugging and aud ### 1. Browser Issues & Audits Chrome automatically checks for common accessibility problems. Use `list_console_messages` to check for these native audits first: -- `types`: `["issue"]` -- `includePreservedMessages`: `true` (to catch issues that occurred during page load) + +- `types`: `["issue"]` +- `includePreservedMessages`: `true` (to catch issues that occurred during page load) This often reveals missing labels, invalid ARIA attributes, and other critical errors without manual investigation. @@ -35,14 +36,24 @@ The accessibility tree exposes the heading hierarchy and semantic landmarks. 3. **Orphaned Inputs**: Verify that all form inputs have associated labels. Use `evaluate_script` to check for inputs missing `id` (for `label[for]`) or `aria-label`: ```javascript () => { - const inputs = Array.from(document.querySelectorAll('input, select, textarea')); - return inputs.filter(i => { - const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); - const hasAria = i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); - const hasImplicitLabel = i.closest('label'); - return !hasId && !hasAria && !hasImplicitLabel; - }).map(i => ({ tag: i.tagName, id: i.id, name: i.name, placeholder: i.placeholder })); - } + const inputs = Array.from( + document.querySelectorAll('input, select, textarea'), + ); + return inputs + .filter(i => { + const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); + const hasAria = + i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); + const hasImplicitLabel = i.closest('label'); + return !hasId && !hasAria && !hasImplicitLabel; + }) + .map(i => ({ + tag: i.tagName, + id: i.id, + name: i.name, + placeholder: i.placeholder, + })); + }; ``` 4. Check images for `alt` text. @@ -54,8 +65,13 @@ Testing "keyboard traps" and proper focus management without visual feedback rel ```javascript () => { const active = document.activeElement; - return { tag: active.tagName, id: active.id, className: active.className, text: active.innerText }; - } + return { + tag: active.tagName, + id: active.id, + className: active.className, + text: active.innerText, + }; + }; ``` 2. Use the `press_key` tool with `"Tab"` or `"Shift+Tab"` to move focus. 3. Re-run the script in step 1 to ensure focus moved to the expected next interactive element. @@ -66,22 +82,25 @@ Testing "keyboard traps" and proper focus management without visual feedback rel According to web.dev, tap targets should be at least 48x48 pixels with sufficient spacing. Since the accessibility tree doesn't show sizes, use `evaluate_script`: ```javascript -(el) => { +el => { const rect = el.getBoundingClientRect(); - return { width: rect.width, height: rect.height }; -} + return {width: rect.width, height: rect.height}; +}; ``` -*Pass the element's `uid` from the snapshot as an argument to the tool.* + +_Pass the element's `uid` from the snapshot as an argument to the tool._ ### 6. Color Contrast -To verify color contrast ratios without the DevTools UI, use `evaluate_script` to compute the relative luminance of the text (`color`) and background (`backgroundColor`). +To verify color contrast ratios without the DevTools UI, use `evaluate_script` to compute the relative luminance of the text (`color`) and background (`backgroundColor`). ```javascript -(el) => { +el => { function getRGB(colorStr) { const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); - return match ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] : [255, 255, 255]; + return match + ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] + : [255, 255, 255]; } function luminance(r, g, b) { const a = [r, g, b].map(function (v) { @@ -90,20 +109,25 @@ To verify color contrast ratios without the DevTools UI, use `evaluate_script` t }); return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; } - + const style = window.getComputedStyle(el); const fg = getRGB(style.color); let bg = getRGB(style.backgroundColor); - + // Basic contrast calculation (Note: Doesn't account for transparency over background images) const l1 = luminance(fg[0], fg[1], fg[2]); const l2 = luminance(bg[0], bg[1], bg[2]); const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); - - return { color: style.color, bg: style.backgroundColor, contrastRatio: ratio.toFixed(2) }; -} + + return { + color: style.color, + bg: style.backgroundColor, + contrastRatio: ratio.toFixed(2), + }; +}; ``` -*Pass the element's `uid` to test the contrast against WCAG AA (4.5:1 for normal text, 3:1 for large text).* + +_Pass the element's `uid` to test the contrast against WCAG AA (4.5:1 for normal text, 3:1 for large text)._ ### 7. Global Page Checks @@ -112,16 +136,22 @@ Verify document-level accessibility settings often missed in component testing: ```javascript () => { return { - lang: document.documentElement.lang || 'MISSING - Screen readers need this for pronunciation', + lang: + document.documentElement.lang || + 'MISSING - Screen readers need this for pronunciation', title: document.title || 'MISSING - Required for context', - viewport: document.querySelector('meta[name="viewport"]')?.content || 'MISSING - Check for user-scalable=no (bad practice)', - reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'Enabled' : 'Disabled' + viewport: + document.querySelector('meta[name="viewport"]')?.content || + 'MISSING - Check for user-scalable=no (bad practice)', + reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches + ? 'Enabled' + : 'Disabled', }; -} +}; ``` ## Troubleshooting If standard a11y queries fail or the `evaluate_script` snippets return unexpected results: -* **Visual Inspection**: If automated scripts cannot determine contrast (e.g., text over gradient images or complex backgrounds), use `take_screenshot` to capture the element. While models cannot measure exact contrast ratios from images, they can visually assess legibility and identifying obvious issues. +- **Visual Inspection**: If automated scripts cannot determine contrast (e.g., text over gradient images or complex backgrounds), use `take_screenshot` to capture the element. While models cannot measure exact contrast ratios from images, they can visually assess legibility and identifying obvious issues. From 97a40470a43db3ff53428d32afa7442e86b56648 Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 11:01:33 +0100 Subject: [PATCH 3/8] docs: Update a11y-debugging skill guide to verify focus using `take_snapshot` instead of `evaluate_script`. --- skills/a11y-debugging/SKILL.md | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index 36021f6b0..991b4afa9 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -59,22 +59,11 @@ The accessibility tree exposes the heading hierarchy and semantic landmarks. ### 4. Focus & Keyboard Navigation -Testing "keyboard traps" and proper focus management without visual feedback relies on precise scripting. +Testing "keyboard traps" and proper focus management without visual feedback relies on tracking the focused element. -1. Use `evaluate_script` to find the currently focused element: - ```javascript - () => { - const active = document.activeElement; - return { - tag: active.tagName, - id: active.id, - className: active.className, - text: active.innerText, - }; - }; - ``` -2. Use the `press_key` tool with `"Tab"` or `"Shift+Tab"` to move focus. -3. Re-run the script in step 1 to ensure focus moved to the expected next interactive element. +1. Use the `press_key` tool with `"Tab"` or `"Shift+Tab"` to move focus. +2. Use `take_snapshot` to capture the updated accessibility tree. +3. Locate the element marked as focused in the snapshot to verify focus moved to the expected interactive element. 4. If a modal opens, focus must move into the modal and "trap" within it until closed. ### 5. Tap Targets and Visuals From 739ffa8e4b45c9743b9fd6adc022ddc1133c5651 Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 11:42:50 +0100 Subject: [PATCH 4/8] docs: Improve color contrast verification guidance by prioritizing native checks and clarifying script limitations. --- skills/a11y-debugging/SKILL.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index 991b4afa9..89918aa69 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -81,7 +81,14 @@ _Pass the element's `uid` from the snapshot as an argument to the tool._ ### 6. Color Contrast -To verify color contrast ratios without the DevTools UI, use `evaluate_script` to compute the relative luminance of the text (`color`) and background (`backgroundColor`). +To verify color contrast ratios, start by checking for native accessibility issues: + +1. Call `list_console_messages` with `types: ["issue"]`. +2. Look for "Low Contrast" issues in the output. + +If native audits do not report issues (which may happen in some headless environments) or if you need to check a specific element manually, you can use the following script as a fallback approximation. + +**Note**: This script uses a simplified algorithm and may not account for transparency, gradients, or background images. For production-grade auditing, consider injecting `axe-core`. ```javascript el => { From fee7e907bea12c7e8db27a7171ca8222382220f7 Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 12:31:07 +0100 Subject: [PATCH 5/8] feat: add tests for the a11y-debugging skill and update its JavaScript snippets to be IIFEs with console logging for manual usage. --- skills/a11y-debugging/SKILL.md | 74 ++++++++++++++++------------ tests/skills/a11y_debugging.test.ts | 75 +++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 tests/skills/a11y_debugging.test.ts diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index 89918aa69..696b9958a 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -35,25 +35,29 @@ The accessibility tree exposes the heading hierarchy and semantic landmarks. 2. Ensure interactive elements have an accessible name (e.g., a button should not just say `""` if it only contains an icon). 3. **Orphaned Inputs**: Verify that all form inputs have associated labels. Use `evaluate_script` to check for inputs missing `id` (for `label[for]`) or `aria-label`: ```javascript - () => { - const inputs = Array.from( - document.querySelectorAll('input, select, textarea'), - ); - return inputs - .filter(i => { - const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); - const hasAria = - i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); - const hasImplicitLabel = i.closest('label'); - return !hasId && !hasAria && !hasImplicitLabel; - }) - .map(i => ({ - tag: i.tagName, - id: i.id, - name: i.name, - placeholder: i.placeholder, - })); - }; + (() => { + const f = () => { + const inputs = Array.from( + document.querySelectorAll('input, select, textarea'), + ); + return inputs + .filter(i => { + const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); + const hasAria = + i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); + const hasImplicitLabel = i.closest('label'); + return !hasId && !hasAria && !hasImplicitLabel; + }) + .map(i => ({ + tag: i.tagName, + id: i.id, + name: i.name, + placeholder: i.placeholder, + })); + }; + try { console.log(f()); } catch(e) {} // Log for manual console usage + return f; + })() ``` 4. Check images for `alt` text. @@ -71,6 +75,7 @@ Testing "keyboard traps" and proper focus management without visual feedback rel According to web.dev, tap targets should be at least 48x48 pixels with sufficient spacing. Since the accessibility tree doesn't show sizes, use `evaluate_script`: ```javascript +// Usage in console: copy, paste, and call with element: fn(element) el => { const rect = el.getBoundingClientRect(); return {width: rect.width, height: rect.height}; @@ -91,6 +96,7 @@ If native audits do not report issues (which may happen in some headless environ **Note**: This script uses a simplified algorithm and may not account for transparency, gradients, or background images. For production-grade auditing, consider injecting `axe-core`. ```javascript +// Usage in console: copy, paste, and call with element: fn(element) el => { function getRGB(colorStr) { const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); @@ -130,20 +136,24 @@ _Pass the element's `uid` to test the contrast against WCAG AA (4.5:1 for normal Verify document-level accessibility settings often missed in component testing: ```javascript -() => { - return { - lang: - document.documentElement.lang || - 'MISSING - Screen readers need this for pronunciation', - title: document.title || 'MISSING - Required for context', - viewport: - document.querySelector('meta[name="viewport"]')?.content || - 'MISSING - Check for user-scalable=no (bad practice)', - reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches - ? 'Enabled' - : 'Disabled', +(() => { + const f = () => { + return { + lang: + document.documentElement.lang || + 'MISSING - Screen readers need this for pronunciation', + title: document.title || 'MISSING - Required for context', + viewport: + document.querySelector('meta[name="viewport"]')?.content || + 'MISSING - Check for user-scalable=no (bad practice)', + reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches + ? 'Enabled' + : 'Disabled', + }; }; -}; + try { console.log(f()); } catch(e) {} // Log for manual console usage + return f; +})() ``` ## Troubleshooting diff --git a/tests/skills/a11y_debugging.test.ts b/tests/skills/a11y_debugging.test.ts new file mode 100644 index 000000000..ec87abd95 --- /dev/null +++ b/tests/skills/a11y_debugging.test.ts @@ -0,0 +1,75 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import assert from 'node:assert'; +import {describe, it} from 'node:test'; +import fs from 'node:fs'; +import path from 'node:path'; + +import {evaluateScript} from '../../src/tools/script.js'; +import {withMcpContext} from '../utils.js'; + +describe('a11y-debugging', () => { + const skillPath = path.join( + process.cwd(), + 'skills', + 'a11y-debugging', + 'SKILL.md', + ); + const skillContent = fs.readFileSync(skillPath, 'utf8'); + + // Extract snippets + // We assume snippets are in ```javascript ... ``` blocks. + const snippets: string[] = []; + const regex = /```javascript([\s\S]*?)```/g; + let match; + while ((match = regex.exec(skillContent)) !== null) { + snippets.push(match[1].trim()); + } + + it('snippets should be valid javascript', () => { + assert.ok(snippets.length > 0, 'Should find snippets in SKILL.md'); + }); + + it('0-arg snippets (IIFEs) should execute with evaluate_script', async () => { + // The 1st snippet (orphaned inputs) and 4th snippet (global checks) are IIFEs returning a function. + // 2nd and 3rd are arg-taking functions (not IIFEs). + const orphanInputsSnippet = snippets[0]; + const globalPageChecksSnippet = snippets[3]; + + assert.ok(orphanInputsSnippet, 'Orphaned inputs snippet not found'); + assert.ok(globalPageChecksSnippet, 'Global page checks snippet not found'); + + await withMcpContext(async (response, context) => { + const page = context.getSelectedPage(); + await page.setContent(''); + + // Test Orphaned Inputs Snippet + await evaluateScript.handler( + {params: {function: orphanInputsSnippet}}, + response, + context + ); + let lineEvaluation = response.responseLines.at(2)!; + let result = JSON.parse(lineEvaluation); + // Expect empty array because we have a valid label + assert.deepStrictEqual(result, []); + + // Test Global Page Checks Snippet + response.resetResponseLineForTesting(); + await evaluateScript.handler( + {params: {function: globalPageChecksSnippet}}, + response, + context + ); + lineEvaluation = response.responseLines.at(2)!; + result = JSON.parse(lineEvaluation); + // We expect some result, just check keys + assert.ok('lang' in result); + assert.ok('title' in result); + }); + }); +}); From 687aa1b8939f6ff2ba2e8ebfd6475b60409922f2 Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 12:53:55 +0100 Subject: [PATCH 6/8] refactor: Simplify JavaScript snippets in a11y-debugging documentation and add debugging logs to a11y tests. --- skills/a11y-debugging/SKILL.md | 77 +++++++++++++---------------- tests/skills/a11y_debugging.test.ts | 53 ++++++++++---------- 2 files changed, 63 insertions(+), 67 deletions(-) diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index 696b9958a..e7e2e9dcd 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -35,30 +35,27 @@ The accessibility tree exposes the heading hierarchy and semantic landmarks. 2. Ensure interactive elements have an accessible name (e.g., a button should not just say `""` if it only contains an icon). 3. **Orphaned Inputs**: Verify that all form inputs have associated labels. Use `evaluate_script` to check for inputs missing `id` (for `label[for]`) or `aria-label`: ```javascript - (() => { - const f = () => { - const inputs = Array.from( - document.querySelectorAll('input, select, textarea'), - ); - return inputs - .filter(i => { - const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); - const hasAria = - i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); - const hasImplicitLabel = i.closest('label'); - return !hasId && !hasAria && !hasImplicitLabel; - }) - .map(i => ({ - tag: i.tagName, - id: i.id, - name: i.name, - placeholder: i.placeholder, - })); - }; - try { console.log(f()); } catch(e) {} // Log for manual console usage - return f; - })() - ``` + () => { + const inputs = Array.from( + document.querySelectorAll('input, select, textarea'), + ); + return inputs + .filter(i => { + const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); + const hasAria = + i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); + const hasImplicitLabel = i.closest('label'); + return !hasId && !hasAria && !hasImplicitLabel; + }) + .map(i => ({ + tag: i.tagName, + id: i.id, + name: i.name, + placeholder: i.placeholder, + })); + } + ``` + 4. Check images for `alt` text. ### 4. Focus & Keyboard Navigation @@ -96,7 +93,6 @@ If native audits do not report issues (which may happen in some headless environ **Note**: This script uses a simplified algorithm and may not account for transparency, gradients, or background images. For production-grade auditing, consider injecting `axe-core`. ```javascript -// Usage in console: copy, paste, and call with element: fn(element) el => { function getRGB(colorStr) { const match = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); @@ -136,24 +132,21 @@ _Pass the element's `uid` to test the contrast against WCAG AA (4.5:1 for normal Verify document-level accessibility settings often missed in component testing: ```javascript -(() => { - const f = () => { - return { - lang: - document.documentElement.lang || - 'MISSING - Screen readers need this for pronunciation', - title: document.title || 'MISSING - Required for context', - viewport: - document.querySelector('meta[name="viewport"]')?.content || - 'MISSING - Check for user-scalable=no (bad practice)', - reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches - ? 'Enabled' - : 'Disabled', - }; +() => { + return { + lang: + document.documentElement.lang || + 'MISSING - Screen readers need this for pronunciation', + title: document.title || 'MISSING - Required for context', + viewport: + document.querySelector('meta[name="viewport"]')?.content || + 'MISSING - Check for user-scalable=no (bad practice)', + reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)') + .matches + ? 'Enabled' + : 'Disabled', }; - try { console.log(f()); } catch(e) {} // Log for manual console usage - return f; -})() +}; ``` ## Troubleshooting diff --git a/tests/skills/a11y_debugging.test.ts b/tests/skills/a11y_debugging.test.ts index ec87abd95..42ac4c279 100644 --- a/tests/skills/a11y_debugging.test.ts +++ b/tests/skills/a11y_debugging.test.ts @@ -5,9 +5,9 @@ */ import assert from 'node:assert'; -import {describe, it} from 'node:test'; import fs from 'node:fs'; import path from 'node:path'; +import { describe, it } from 'node:test'; import {evaluateScript} from '../../src/tools/script.js'; import {withMcpContext} from '../utils.js'; @@ -44,32 +44,35 @@ describe('a11y-debugging', () => { assert.ok(globalPageChecksSnippet, 'Global page checks snippet not found'); await withMcpContext(async (response, context) => { - const page = context.getSelectedPage(); - await page.setContent(''); + const page = context.getSelectedPage(); + await page.setContent(''); - // Test Orphaned Inputs Snippet - await evaluateScript.handler( - {params: {function: orphanInputsSnippet}}, - response, - context - ); - let lineEvaluation = response.responseLines.at(2)!; - let result = JSON.parse(lineEvaluation); - // Expect empty array because we have a valid label - assert.deepStrictEqual(result, []); + // Test Orphaned Inputs Snippet + await evaluateScript.handler( + { params: { function: orphanInputsSnippet } }, + response, + context, + ); + let lineEvaluation = response.responseLines.at(2)!; + let result = JSON.parse(lineEvaluation); + // Expect empty array because we have a valid label + assert.deepStrictEqual(result, []); - // Test Global Page Checks Snippet - response.resetResponseLineForTesting(); - await evaluateScript.handler( - {params: {function: globalPageChecksSnippet}}, - response, - context - ); - lineEvaluation = response.responseLines.at(2)!; - result = JSON.parse(lineEvaluation); - // We expect some result, just check keys - assert.ok('lang' in result); - assert.ok('title' in result); + // Test Global Page Checks Snippet + response.resetResponseLineForTesting(); + await evaluateScript.handler( + { params: { function: globalPageChecksSnippet } }, + response, + context, + ); + console.log('Global Page Checks Snippet:', globalPageChecksSnippet); + const output = response.toString(); + console.log('Response Output:', output); + lineEvaluation = response.responseLines.at(2)!; + result = JSON.parse(lineEvaluation); + // We expect some result, just check keys + assert.ok('lang' in result); + assert.ok('title' in result); }); }); }); From 354be91b43ea1a2a770285bb18febc5408b4288d Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 12:56:49 +0100 Subject: [PATCH 7/8] feat: delete a11y_debugging.test.ts --- skills/a11y-debugging/SKILL.md | 8 ++- tests/skills/a11y_debugging.test.ts | 78 ----------------------------- 2 files changed, 7 insertions(+), 79 deletions(-) delete mode 100644 tests/skills/a11y_debugging.test.ts diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index e7e2e9dcd..e3b83e803 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -132,7 +132,8 @@ _Pass the element's `uid` to test the contrast against WCAG AA (4.5:1 for normal Verify document-level accessibility settings often missed in component testing: ```javascript -() => { +(() => { + const f = () => { return { lang: document.documentElement.lang || @@ -147,6 +148,11 @@ Verify document-level accessibility settings often missed in component testing: : 'Disabled', }; }; + try { + console.log(f()); + } catch (e) {} // Log for manual console usage + return f; +})(); ``` ## Troubleshooting diff --git a/tests/skills/a11y_debugging.test.ts b/tests/skills/a11y_debugging.test.ts deleted file mode 100644 index 42ac4c279..000000000 --- a/tests/skills/a11y_debugging.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import assert from 'node:assert'; -import fs from 'node:fs'; -import path from 'node:path'; -import { describe, it } from 'node:test'; - -import {evaluateScript} from '../../src/tools/script.js'; -import {withMcpContext} from '../utils.js'; - -describe('a11y-debugging', () => { - const skillPath = path.join( - process.cwd(), - 'skills', - 'a11y-debugging', - 'SKILL.md', - ); - const skillContent = fs.readFileSync(skillPath, 'utf8'); - - // Extract snippets - // We assume snippets are in ```javascript ... ``` blocks. - const snippets: string[] = []; - const regex = /```javascript([\s\S]*?)```/g; - let match; - while ((match = regex.exec(skillContent)) !== null) { - snippets.push(match[1].trim()); - } - - it('snippets should be valid javascript', () => { - assert.ok(snippets.length > 0, 'Should find snippets in SKILL.md'); - }); - - it('0-arg snippets (IIFEs) should execute with evaluate_script', async () => { - // The 1st snippet (orphaned inputs) and 4th snippet (global checks) are IIFEs returning a function. - // 2nd and 3rd are arg-taking functions (not IIFEs). - const orphanInputsSnippet = snippets[0]; - const globalPageChecksSnippet = snippets[3]; - - assert.ok(orphanInputsSnippet, 'Orphaned inputs snippet not found'); - assert.ok(globalPageChecksSnippet, 'Global page checks snippet not found'); - - await withMcpContext(async (response, context) => { - const page = context.getSelectedPage(); - await page.setContent(''); - - // Test Orphaned Inputs Snippet - await evaluateScript.handler( - { params: { function: orphanInputsSnippet } }, - response, - context, - ); - let lineEvaluation = response.responseLines.at(2)!; - let result = JSON.parse(lineEvaluation); - // Expect empty array because we have a valid label - assert.deepStrictEqual(result, []); - - // Test Global Page Checks Snippet - response.resetResponseLineForTesting(); - await evaluateScript.handler( - { params: { function: globalPageChecksSnippet } }, - response, - context, - ); - console.log('Global Page Checks Snippet:', globalPageChecksSnippet); - const output = response.toString(); - console.log('Response Output:', output); - lineEvaluation = response.responseLines.at(2)!; - result = JSON.parse(lineEvaluation); - // We expect some result, just check keys - assert.ok('lang' in result); - assert.ok('title' in result); - }); - }); -}); From c8addd2f0a8a8f18462a3a88baa31df795b455e5 Mon Sep 17 00:00:00 2001 From: Michael Hablich Date: Fri, 20 Feb 2026 13:02:28 +0100 Subject: [PATCH 8/8] Reformat --- skills/a11y-debugging/SKILL.md | 72 +++++++++++++++++----------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/skills/a11y-debugging/SKILL.md b/skills/a11y-debugging/SKILL.md index e3b83e803..0d2af3e47 100644 --- a/skills/a11y-debugging/SKILL.md +++ b/skills/a11y-debugging/SKILL.md @@ -36,25 +36,27 @@ The accessibility tree exposes the heading hierarchy and semantic landmarks. 3. **Orphaned Inputs**: Verify that all form inputs have associated labels. Use `evaluate_script` to check for inputs missing `id` (for `label[for]`) or `aria-label`: ```javascript () => { - const inputs = Array.from( - document.querySelectorAll('input, select, textarea'), - ); - return inputs - .filter(i => { - const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); - const hasAria = - i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); - const hasImplicitLabel = i.closest('label'); - return !hasId && !hasAria && !hasImplicitLabel; - }) - .map(i => ({ - tag: i.tagName, - id: i.id, - name: i.name, - placeholder: i.placeholder, - })); - } - ``` + const inputs = Array.from( + document.querySelectorAll('input, select, textarea'), + ); + return inputs + .filter(i => { + const hasId = i.id && document.querySelector(`label[for="${i.id}"]`); + const hasAria = + i.getAttribute('aria-label') || i.getAttribute('aria-labelledby'); + const hasImplicitLabel = i.closest('label'); + return !hasId && !hasAria && !hasImplicitLabel; + }) + .map(i => ({ + tag: i.tagName, + id: i.id, + name: i.name, + placeholder: i.placeholder, + })); + }; + ``` + +```` 4. Check images for `alt` text. @@ -74,10 +76,10 @@ According to web.dev, tap targets should be at least 48x48 pixels with sufficien ```javascript // Usage in console: copy, paste, and call with element: fn(element) el => { - const rect = el.getBoundingClientRect(); - return {width: rect.width, height: rect.height}; + const rect = el.getBoundingClientRect(); + return {width: rect.width, height: rect.height}; }; -``` +```` _Pass the element's `uid` from the snapshot as an argument to the tool._ @@ -134,20 +136,20 @@ Verify document-level accessibility settings often missed in component testing: ```javascript (() => { const f = () => { - return { - lang: - document.documentElement.lang || - 'MISSING - Screen readers need this for pronunciation', - title: document.title || 'MISSING - Required for context', - viewport: - document.querySelector('meta[name="viewport"]')?.content || - 'MISSING - Check for user-scalable=no (bad practice)', - reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)') - .matches - ? 'Enabled' - : 'Disabled', + return { + lang: + document.documentElement.lang || + 'MISSING - Screen readers need this for pronunciation', + title: document.title || 'MISSING - Required for context', + viewport: + document.querySelector('meta[name="viewport"]')?.content || + 'MISSING - Check for user-scalable=no (bad practice)', + reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)') + .matches + ? 'Enabled' + : 'Disabled', + }; }; -}; try { console.log(f()); } catch (e) {} // Log for manual console usage