chore: run a11y audits on protractor (#2010)

* chore: run a11y audits on protractor

* Fixes accessibility issue in grid-list with `role="listitem"`
* Adds protractor plugin to run aXe-core accessibility audits
* Fixes a bunch of a11y issues in the e2e app to make the aXe audits green

* Force menu y-positions because viewport might be different on Selenium browsers.

* Navigiation links should be hidden by default because those can interfere with the tests.
This commit is contained in:
Paul Gschwendtner
2016-12-13 21:10:31 +01:00
committed by Jeremy Elbourn
parent 66c25be710
commit c5fb94f2a4
10 changed files with 112 additions and 22 deletions
+2 -2
View File
@@ -53,8 +53,8 @@ export class MenuPage {
expectMenuLocation(el: ElementFinder, {x, y}: {x: number, y: number}) {
el.getLocation().then(loc => {
expect(loc.x).toEqual(x);
expect(loc.y).toEqual(y);
expect(loc.x).toEqual(x, 'Expect the x-position to be equal');
expect(loc.y).toEqual(y, 'Expect the y-position to be equal');
});
}
+2 -1
View File
@@ -51,6 +51,8 @@
"@types/node": "^6.0.34",
"@types/run-sequence": "0.0.27",
"@types/rx": "^2.5.33",
"axe-core": "^2.0.7",
"axe-webdriverjs": "^0.4.0",
"conventional-changelog": "^1.1.0",
"express": "^4.14.0",
"firebase-tools": "^2.2.1",
@@ -79,7 +81,6 @@
"minimist": "^1.2.0",
"node-sass": "^3.4.2",
"protractor": "^4.0.8",
"protractor-accessibility-plugin": "0.1.1",
"resolve-bin": "^0.4.0",
"rollup": "^0.34.13",
"run-sequence": "^1.2.2",
+19 -13
View File
@@ -1,14 +1,20 @@
<a md-list-item [routerLink]="['button']">Button</a>
<a md-list-item [routerLink]="['checkbox']">Checkbox</a>
<a md-list-item [routerLink]="['dialog']">Dialog</a>
<a md-list-item [routerLink]="['grid-list']">Grid list</a>
<a md-list-item [routerLink]="['icon']">Icon</a>
<a md-list-item [routerLink]="['list']">List</a>
<a md-list-item [routerLink]="['menu']">Menu</a>
<a md-list-item [routerLink]="['progress-bar']">Progress bar</a>
<a md-list-item [routerLink]="['progress-circle']">Progress circle</a>
<a md-list-item [routerLink]="['radio']">Radios</a>
<a md-list-item [routerLink]="['slide-toggle']">Slide Toggle</a>
<a md-list-item [routerLink]="['tabs']">Tabs</a>
<button (click)="showLinks = !showLinks">Toggle Navigation Links</button>
<router-outlet></router-outlet>
<md-nav-list *ngIf="showLinks">
<a md-list-item [routerLink]="['button']">Button</a>
<a md-list-item [routerLink]="['checkbox']">Checkbox</a>
<a md-list-item [routerLink]="['dialog']">Dialog</a>
<a md-list-item [routerLink]="['grid-list']">Grid list</a>
<a md-list-item [routerLink]="['icon']">Icon</a>
<a md-list-item [routerLink]="['list']">List</a>
<a md-list-item [routerLink]="['menu']">Menu</a>
<a md-list-item [routerLink]="['progress-bar']">Progress bar</a>
<a md-list-item [routerLink]="['progress-circle']">Progress circle</a>
<a md-list-item [routerLink]="['radio']">Radios</a>
<a md-list-item [routerLink]="['slide-toggle']">Slide Toggle</a>
<a md-list-item [routerLink]="['tabs']">Tabs</a>
</md-nav-list>
<main>
<router-outlet role="main"></router-outlet>
</main>
+3 -1
View File
@@ -12,4 +12,6 @@ export class Home {}
selector: 'e2e-app',
templateUrl: 'e2e-app.html',
})
export class E2EApp { }
export class E2EApp {
showLinks: boolean = false;
}
+2 -2
View File
@@ -5,7 +5,7 @@
<button [md-menu-trigger-for]="menu" id="trigger">TRIGGER</button>
<button [md-menu-trigger-for]="menu" id="trigger-two">TRIGGER 2</button>
<md-menu #menu="mdMenu" class="custom">
<md-menu #menu="mdMenu" y-position="below" class="custom">
<button md-menu-item (click)="selected='one'">One</button>
<button md-menu-item (click)="selected='two'">Two</button>
<button md-menu-item (click)="selected='three'" disabled>Three</button>
@@ -15,7 +15,7 @@
<button [md-menu-trigger-for]="beforeMenu" id="before-t">
BEFORE
</button>
<md-menu x-position="before" class="before" #beforeMenu="mdMenu">
<md-menu x-position="before" y-position="below" class="before" #beforeMenu="mdMenu">
<button md-menu-item>Item</button>
</md-menu>
+3 -1
View File
@@ -1,10 +1,12 @@
<section>
<md-radio-group [disabled]="isGroupDisabled"
[(value)]="groupValue"
id="test-group">
id="test-group" aria-label="Select a Pokemon">
<md-radio-button value="fire" id="fire">Charmander</md-radio-button>
<md-radio-button value="water" id="water">Squirtle</md-radio-button>
<md-radio-button value="leaf" id="leaf">Bulbasaur</md-radio-button>
</md-radio-group>
<button (click)="isGroupDisabled=!isGroupDisabled" id="toggle-disable">Disable/enable group</button>
</section>
+3
View File
@@ -34,6 +34,9 @@ const MD_FIT_MODE = 'fit';
selector: 'md-grid-list, mat-grid-list',
templateUrl: 'grid-list.html',
styleUrls: ['grid-list.css'],
host: {
'role': 'list'
},
encapsulation: ViewEncapsulation.None,
})
export class MdGridList implements OnInit, AfterContentChecked {
+14 -2
View File
@@ -9,7 +9,6 @@ require('ts-node').register({
const E2E_BASE_URL = process.env['E2E_BASE_URL'] || 'http://localhost:4200';
const config = {
// TODO(jelbourn): add back plugin for a11y assersions once it supports specifying AXS options.
useAllAngular2AppRoots: true,
specs: [ path.join(__dirname, '../e2e/**/*.e2e.ts') ],
baseUrl: E2E_BASE_URL,
@@ -17,7 +16,20 @@ const config = {
getPageTimeout: 120000,
jasmineNodeOpts: {
defaultTimeoutInterval: 120000,
}
},
plugins: [
{
// Runs the axe-core accessibility checks each time the e2e page changes and
// Angular is ready.
path: '../tools/axe-protractor/axe-protractor.js',
rules: [
// Exclude md-menu elements because those are empty if not active.
{ id: 'aria-required-children', selector: '*:not(md-menu)' },
]
}
]
};
if (process.env['TRAVIS']) {
+48
View File
@@ -0,0 +1,48 @@
'use strict';
/**
* Protractor Plugin to run axe-core accessibility audits after Angular bootstrapped.
*/
const AxeBuilder = require('axe-webdriverjs');
const {buildMessage} = require('./build-message');
/* List of pages which were already checked by axe-core and shouldn't run again */
const checkedPages = [];
/**
* Protractor plugin hook which always runs when Angular successfully bootstrapped.
*/
function onPageStable() {
AxeBuilder(browser.driver)
.configure(this.config || {})
.analyze(results => handleResults(this, results));
}
/**
* Processes the axe-core results by reporting recognized violations
* to Protractor and printing them out.
* @param {!protractor.ProtractorPlugin} context
* @param {!axe.AxeResults} results
*/
function handleResults(context, results) {
if (checkedPages.indexOf(results.url) === -1) {
checkedPages.push(results.url);
results.violations.forEach(violation => {
let specName = `${violation.help} (${results.url})`;
let message = '\n' + buildMessage(violation);
context.addFailure(message, {specName});
});
}
}
exports.name = 'protractor-axe';
exports.onPageStable = onPageStable;
+16
View File
@@ -0,0 +1,16 @@
/**
* Builds a simple message of the violation results of axe-core by listing
* each violation and the associated element selector in a new line.
* @param {!axe.Violation} violation
*/
exports.buildMessage = violation => {
let selectors = violation.nodes.map(node => {
return node.target.join(' ');
});
return selectors.reduce((content, selector) => {
return content + '- ' + selector + '\n';
}, '');
};