From c5fb94f2a44f3a36a2cb724dfafc8f5a64de7b58 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Tue, 13 Dec 2016 21:10:31 +0100 Subject: [PATCH] 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. --- e2e/components/menu/menu-page.ts | 4 +-- package.json | 3 +- src/e2e-app/e2e-app/e2e-app.html | 32 ++++++++++------- src/e2e-app/e2e-app/e2e-app.ts | 4 ++- src/e2e-app/menu/menu-e2e.html | 4 +-- src/e2e-app/radio/radio-e2e.html | 4 ++- src/lib/grid-list/grid-list.ts | 3 ++ test/protractor.conf.js | 16 +++++++-- tools/axe-protractor/axe-protractor.js | 48 ++++++++++++++++++++++++++ tools/axe-protractor/build-message.js | 16 +++++++++ 10 files changed, 112 insertions(+), 22 deletions(-) create mode 100644 tools/axe-protractor/axe-protractor.js create mode 100644 tools/axe-protractor/build-message.js diff --git a/e2e/components/menu/menu-page.ts b/e2e/components/menu/menu-page.ts index 5470c483117..a89ce732757 100644 --- a/e2e/components/menu/menu-page.ts +++ b/e2e/components/menu/menu-page.ts @@ -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'); }); } diff --git a/package.json b/package.json index e7ad7f2d5fe..9daa4e0f898 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/e2e-app/e2e-app/e2e-app.html b/src/e2e-app/e2e-app/e2e-app.html index 6345cffd70f..ae6cffdbbff 100644 --- a/src/e2e-app/e2e-app/e2e-app.html +++ b/src/e2e-app/e2e-app/e2e-app.html @@ -1,14 +1,20 @@ -Button -Checkbox -Dialog -Grid list -Icon -List -Menu -Progress bar -Progress circle -Radios -Slide Toggle -Tabs + - + + Button + Checkbox + Dialog + Grid list + Icon + List + Menu + Progress bar + Progress circle + Radios + Slide Toggle + Tabs + + +
+ +
diff --git a/src/e2e-app/e2e-app/e2e-app.ts b/src/e2e-app/e2e-app/e2e-app.ts index 95f768e69df..64fb2e4504a 100644 --- a/src/e2e-app/e2e-app/e2e-app.ts +++ b/src/e2e-app/e2e-app/e2e-app.ts @@ -12,4 +12,6 @@ export class Home {} selector: 'e2e-app', templateUrl: 'e2e-app.html', }) -export class E2EApp { } +export class E2EApp { + showLinks: boolean = false; +} diff --git a/src/e2e-app/menu/menu-e2e.html b/src/e2e-app/menu/menu-e2e.html index 057517b9c8f..ad1c2f86c81 100644 --- a/src/e2e-app/menu/menu-e2e.html +++ b/src/e2e-app/menu/menu-e2e.html @@ -5,7 +5,7 @@ - + @@ -15,7 +15,7 @@ - + diff --git a/src/e2e-app/radio/radio-e2e.html b/src/e2e-app/radio/radio-e2e.html index 83d672df16c..d14f9cead51 100644 --- a/src/e2e-app/radio/radio-e2e.html +++ b/src/e2e-app/radio/radio-e2e.html @@ -1,10 +1,12 @@
+ id="test-group" aria-label="Select a Pokemon"> + Charmander Squirtle Bulbasaur +
diff --git a/src/lib/grid-list/grid-list.ts b/src/lib/grid-list/grid-list.ts index 94d3ae9ce36..2fb9208d162 100644 --- a/src/lib/grid-list/grid-list.ts +++ b/src/lib/grid-list/grid-list.ts @@ -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 { diff --git a/test/protractor.conf.js b/test/protractor.conf.js index 5a9c2c63100..c613290a021 100644 --- a/test/protractor.conf.js +++ b/test/protractor.conf.js @@ -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']) { diff --git a/tools/axe-protractor/axe-protractor.js b/tools/axe-protractor/axe-protractor.js new file mode 100644 index 00000000000..ca18cc17c86 --- /dev/null +++ b/tools/axe-protractor/axe-protractor.js @@ -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; diff --git a/tools/axe-protractor/build-message.js b/tools/axe-protractor/build-message.js new file mode 100644 index 00000000000..be07a9bb52d --- /dev/null +++ b/tools/axe-protractor/build-message.js @@ -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'; + }, ''); + +}; \ No newline at end of file