Skip to content

Commit dff97cf

Browse files
authored
add editor a11y playwright tests (microsoft#255899)
1 parent dd0856d commit dff97cf

File tree

4 files changed

+252
-0
lines changed

4 files changed

+252
-0
lines changed

src/vs/editor/browser/controller/editContext/native/nativeEditContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export class NativeEditContext extends AbstractEditContext {
9090
this._imeTextArea.setClassName(`ime-text-area`);
9191
this._imeTextArea.setAttribute('readonly', 'true');
9292
this._imeTextArea.setAttribute('tabindex', '-1');
93+
this._imeTextArea.setAttribute('aria-hidden', 'true');
9394
this.domNode.setAttribute('autocorrect', 'off');
9495
this.domNode.setAttribute('autocapitalize', 'off');
9596
this.domNode.setAttribute('autocomplete', 'off');

test/monaco/monaco.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import * as playwright from '@playwright/test';
77
import { assert } from 'chai';
8+
import { injectAxe } from 'axe-playwright';
89

910
const PORT = 8563;
1011
const TIMEOUT = 20 * 1000;
@@ -135,4 +136,127 @@ describe('API Integration Tests', function (): void {
135136
'\t\t\'\'\'Make the monkey eat N bananas!\'\'\''
136137
]);
137138
});
139+
describe('Accessibility', function (): void {
140+
beforeEach(async () => {
141+
await page.goto(APP);
142+
await injectAxe(page);
143+
await page.evaluate(`
144+
(function () {
145+
instance.focus();
146+
instance.trigger('keyboard', 'cursorHome');
147+
instance.trigger('keyboard', 'type', {
148+
text: 'a'
149+
});
150+
})()
151+
`);
152+
});
153+
154+
it('Editor should not have critical accessibility violations', async () => {
155+
let violationCount = 0;
156+
const checkedElements = new Set<string>();
157+
158+
// Run axe and get all results (passes and violations)
159+
const axeResults = await page.evaluate(() => {
160+
return window.axe.run(document, {
161+
runOnly: {
162+
type: 'tag',
163+
values: [
164+
'wcag2a',
165+
'wcag2aa',
166+
'wcag21a',
167+
'wcag21aa',
168+
'best-practice'
169+
]
170+
}
171+
});
172+
});
173+
174+
axeResults.violations.forEach((v: any) => {
175+
const isCritical = v.impact === 'critical';
176+
const emoji = isCritical ? '❌' : undefined;
177+
v.nodes.forEach((node: any) => {
178+
const selector = node.target?.join(' ');
179+
if (selector && emoji) {
180+
checkedElements.add(selector);
181+
console.log(`${emoji} FAIL: ${selector} - ${v.id} - ${v.description}`);
182+
}
183+
});
184+
violationCount += isCritical ? 1 : 0;
185+
});
186+
187+
axeResults.passes.forEach((pass: any) => {
188+
pass.nodes.forEach((node: any) => {
189+
const selector = node.target?.join(' ');
190+
if (selector && !checkedElements.has(selector)) {
191+
checkedElements.add(selector);
192+
}
193+
});
194+
});
195+
196+
playwright.expect(violationCount).toBe(0);
197+
});
198+
199+
it('Editor should not have color contrast accessibility violations', async () => {
200+
let violationCount = 0;
201+
const checkedElements = new Set<string>();
202+
203+
const axeResults = await page.evaluate(() => {
204+
return window.axe.run(document, {
205+
runOnly: {
206+
type: 'rule',
207+
values: ['color-contrast']
208+
}
209+
});
210+
});
211+
212+
axeResults.violations.forEach((v: any) => {
213+
const isCritical = v.impact === 'critical';
214+
const emoji = isCritical ? '❌' : undefined;
215+
v.nodes.forEach((node: any) => {
216+
const selector = node.target?.join(' ');
217+
if (selector && emoji) {
218+
checkedElements.add(selector);
219+
console.log(`${emoji} FAIL: ${selector} - ${v.id} - ${v.description}`);
220+
}
221+
});
222+
violationCount += 1;
223+
});
224+
225+
axeResults.passes.forEach((pass: any) => {
226+
pass.nodes.forEach((node: any) => {
227+
const selector = node.target?.join(' ');
228+
if (selector && !checkedElements.has(selector)) {
229+
checkedElements.add(selector);
230+
}
231+
});
232+
});
233+
234+
playwright.expect(violationCount).toBe(0);
235+
});
236+
it('Monaco editor container should have an ARIA role', async () => {
237+
const role = await page.evaluate(() => {
238+
const container = document.querySelector('.monaco-editor');
239+
return container?.getAttribute('role');
240+
});
241+
assert.isDefined(role, 'Monaco editor container should have a role attribute');
242+
});
243+
244+
it('Monaco editor should have an ARIA label', async () => {
245+
const ariaLabel = await page.evaluate(() => {
246+
const container = document.querySelector('.monaco-editor');
247+
return container?.getAttribute('aria-label');
248+
});
249+
assert.isDefined(ariaLabel, 'Monaco editor container should have an aria-label attribute');
250+
});
251+
252+
it('All toolbar buttons should have accessible names', async () => {
253+
const buttonsWithoutLabel = await page.evaluate(() => {
254+
return Array.from(document.querySelectorAll('button')).filter(btn => {
255+
const label = btn.getAttribute('aria-label') || btn.textContent?.trim();
256+
return !label;
257+
}).map(btn => btn.outerHTML);
258+
});
259+
assert.deepEqual(buttonsWithoutLabel, [], 'All toolbar buttons should have accessible names');
260+
});
261+
});
138262
});

test/monaco/package-lock.json

Lines changed: 126 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/monaco/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
},
1313
"devDependencies": {
1414
"@types/chai": "^4.2.14",
15+
"axe-playwright": "^2.1.0",
1516
"chai": "^4.2.0",
1617
"warnings-to-errors-webpack-plugin": "^2.3.0"
1718
}

0 commit comments

Comments
 (0)