Skip to content

Commit fee7e90

Browse files
committed
feat: add tests for the a11y-debugging skill and update its JavaScript snippets to be IIFEs with console logging for manual usage.
1 parent 739ffa8 commit fee7e90

2 files changed

Lines changed: 117 additions & 32 deletions

File tree

skills/a11y-debugging/SKILL.md

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,25 +35,29 @@ The accessibility tree exposes the heading hierarchy and semantic landmarks.
3535
2. Ensure interactive elements have an accessible name (e.g., a button should not just say `""` if it only contains an icon).
3636
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`:
3737
```javascript
38-
() => {
39-
const inputs = Array.from(
40-
document.querySelectorAll('input, select, textarea'),
41-
);
42-
return inputs
43-
.filter(i => {
44-
const hasId = i.id && document.querySelector(`label[for="${i.id}"]`);
45-
const hasAria =
46-
i.getAttribute('aria-label') || i.getAttribute('aria-labelledby');
47-
const hasImplicitLabel = i.closest('label');
48-
return !hasId && !hasAria && !hasImplicitLabel;
49-
})
50-
.map(i => ({
51-
tag: i.tagName,
52-
id: i.id,
53-
name: i.name,
54-
placeholder: i.placeholder,
55-
}));
56-
};
38+
(() => {
39+
const f = () => {
40+
const inputs = Array.from(
41+
document.querySelectorAll('input, select, textarea'),
42+
);
43+
return inputs
44+
.filter(i => {
45+
const hasId = i.id && document.querySelector(`label[for="${i.id}"]`);
46+
const hasAria =
47+
i.getAttribute('aria-label') || i.getAttribute('aria-labelledby');
48+
const hasImplicitLabel = i.closest('label');
49+
return !hasId && !hasAria && !hasImplicitLabel;
50+
})
51+
.map(i => ({
52+
tag: i.tagName,
53+
id: i.id,
54+
name: i.name,
55+
placeholder: i.placeholder,
56+
}));
57+
};
58+
try { console.log(f()); } catch(e) {} // Log for manual console usage
59+
return f;
60+
})()
5761
```
5862
4. Check images for `alt` text.
5963

@@ -71,6 +75,7 @@ Testing "keyboard traps" and proper focus management without visual feedback rel
7175
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`:
7276
7377
```javascript
78+
// Usage in console: copy, paste, and call with element: fn(element)
7479
el => {
7580
const rect = el.getBoundingClientRect();
7681
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
9196
**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`.
9297

9398
```javascript
99+
// Usage in console: copy, paste, and call with element: fn(element)
94100
el => {
95101
function getRGB(colorStr) {
96102
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
130136
Verify document-level accessibility settings often missed in component testing:
131137
132138
```javascript
133-
() => {
134-
return {
135-
lang:
136-
document.documentElement.lang ||
137-
'MISSING - Screen readers need this for pronunciation',
138-
title: document.title || 'MISSING - Required for context',
139-
viewport:
140-
document.querySelector('meta[name="viewport"]')?.content ||
141-
'MISSING - Check for user-scalable=no (bad practice)',
142-
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches
143-
? 'Enabled'
144-
: 'Disabled',
139+
(() => {
140+
const f = () => {
141+
return {
142+
lang:
143+
document.documentElement.lang ||
144+
'MISSING - Screen readers need this for pronunciation',
145+
title: document.title || 'MISSING - Required for context',
146+
viewport:
147+
document.querySelector('meta[name="viewport"]')?.content ||
148+
'MISSING - Check for user-scalable=no (bad practice)',
149+
reducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches
150+
? 'Enabled'
151+
: 'Disabled',
152+
};
145153
};
146-
};
154+
try { console.log(f()); } catch(e) {} // Log for manual console usage
155+
return f;
156+
})()
147157
```
148158
149159
## Troubleshooting
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import {describe, it} from 'node:test';
9+
import fs from 'node:fs';
10+
import path from 'node:path';
11+
12+
import {evaluateScript} from '../../src/tools/script.js';
13+
import {withMcpContext} from '../utils.js';
14+
15+
describe('a11y-debugging', () => {
16+
const skillPath = path.join(
17+
process.cwd(),
18+
'skills',
19+
'a11y-debugging',
20+
'SKILL.md',
21+
);
22+
const skillContent = fs.readFileSync(skillPath, 'utf8');
23+
24+
// Extract snippets
25+
// We assume snippets are in ```javascript ... ``` blocks.
26+
const snippets: string[] = [];
27+
const regex = /```javascript([\s\S]*?)```/g;
28+
let match;
29+
while ((match = regex.exec(skillContent)) !== null) {
30+
snippets.push(match[1].trim());
31+
}
32+
33+
it('snippets should be valid javascript', () => {
34+
assert.ok(snippets.length > 0, 'Should find snippets in SKILL.md');
35+
});
36+
37+
it('0-arg snippets (IIFEs) should execute with evaluate_script', async () => {
38+
// The 1st snippet (orphaned inputs) and 4th snippet (global checks) are IIFEs returning a function.
39+
// 2nd and 3rd are arg-taking functions (not IIFEs).
40+
const orphanInputsSnippet = snippets[0];
41+
const globalPageChecksSnippet = snippets[3];
42+
43+
assert.ok(orphanInputsSnippet, 'Orphaned inputs snippet not found');
44+
assert.ok(globalPageChecksSnippet, 'Global page checks snippet not found');
45+
46+
await withMcpContext(async (response, context) => {
47+
const page = context.getSelectedPage();
48+
await page.setContent('<input id="foo"><label for="foo">Foo</label>');
49+
50+
// Test Orphaned Inputs Snippet
51+
await evaluateScript.handler(
52+
{params: {function: orphanInputsSnippet}},
53+
response,
54+
context
55+
);
56+
let lineEvaluation = response.responseLines.at(2)!;
57+
let result = JSON.parse(lineEvaluation);
58+
// Expect empty array because we have a valid label
59+
assert.deepStrictEqual(result, []);
60+
61+
// Test Global Page Checks Snippet
62+
response.resetResponseLineForTesting();
63+
await evaluateScript.handler(
64+
{params: {function: globalPageChecksSnippet}},
65+
response,
66+
context
67+
);
68+
lineEvaluation = response.responseLines.at(2)!;
69+
result = JSON.parse(lineEvaluation);
70+
// We expect some result, just check keys
71+
assert.ok('lang' in result);
72+
assert.ok('title' in result);
73+
});
74+
});
75+
});

0 commit comments

Comments
 (0)