├── .eslintignore
├── test
├── iframe2.html
├── iframe.html
├── js
│ ├── browser-utils-test.js
│ ├── color-test.js
│ ├── audit-rules-test.js
│ ├── gui-utils-test.js
│ └── audit-rule-test.js
├── audits
│ ├── human-lang-missing-test.js
│ ├── page-without-title-test.js
│ ├── tab-index-greater-than-zero-test.js
│ ├── multiple-labelable-elements-per-label-test.js
│ ├── bad-aria-role-test.js
│ ├── bad-aria-attribute-value-test.js
│ ├── focusable-element-not-visible-not-aria-hidden-test.js
│ ├── focusable-element-not-visible-not-aria-hidden-test-browser.js
│ ├── main-role-on-inappropriate-element-test.js
│ ├── image-without-alt-text-test.js
│ ├── aria-role-not-scoped-test.js
│ ├── aria-owns-descendant-test.js
│ ├── required-owned-aria-role-missing-test.js
│ ├── role-tooltip-requires-described-by-test.js
│ ├── multiple-aria-owners-test.js
│ ├── non-existent-aria-related-element-test.js
│ ├── aria-on-reserved-element-test.js
│ ├── uncontrolled-tabpanel-test.js
│ ├── link-with-unclear-purpose.js
│ ├── required-aria-attribute-missing-test.js
│ └── bad-aria-attribute-test.js
├── gui-browser.html
├── testUtils.js
├── index.html
└── qunit.css
├── .gitmodules
├── .gitignore
├── .travis.yml
├── main.js
├── src
├── js
│ ├── axs.js
│ ├── externs
│ │ └── externs.js
│ ├── BrowserUtils.js
│ ├── AuditRules.js
│ └── AuditResults.js
└── audits
│ ├── HumanLangMissing.js
│ ├── TabIndexGreaterThanZero.js
│ ├── VideoWithoutCaptions.js
│ ├── AriaOnReservedElement.js
│ ├── AudioWithoutControls.js
│ ├── PageWithoutTitle.js
│ ├── BadAriaRole.js
│ ├── MultipleLabelableElementsPerLabel.js
│ ├── LowContrast.js
│ ├── RoleTooltipRequiresDescribedBy.js
│ ├── MainRoleOnInappropriateElement.js
│ ├── NonExistentAriaRelatedElement.js
│ ├── ImageWithoutAltText.js
│ ├── UnfocusableElementsWithOnClick.js
│ ├── MeaningfulBackgroundImage.js
│ ├── BadAriaAttributeValue.js
│ ├── MultipleAriaOwners.js
│ ├── AriaOwnsDescendant.js
│ ├── RequiredAriaAttributeMissing.js
│ ├── UncontrolledTabpanel.js
│ ├── FocusableElementNotVisibleAndNotAriaHidden.js
│ ├── BadAriaAttribute.js
│ ├── DuplicateId.js
│ ├── ControlsWithoutLabel.js
│ ├── AriaRoleNotScoped.js
│ ├── LinkWithUnclearPurpose.js
│ ├── UnsupportedAriaAttribute.js
│ ├── RequiredOwnedAriaRoleMissing.js
│ └── TableHasAppropriateHeaders.js
├── bower.json
├── scripts
├── output_wrapper.txt
├── gh_repo.coffee
├── parse_aria_schemas.py
└── aria_rdf_to_constants.xsl
├── package.json
├── .eslintrc
└── tools
└── runner
└── audit.js
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/js/externs/**
2 |
--------------------------------------------------------------------------------
/test/iframe2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Some text in nested iframe
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/closure-library"]
2 | path = lib/closure-library
3 | url = https://github.com/google/closure-library
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | extension/generated_accessibility.js
2 | key.pem
3 | extension/Handlebar.js
4 | upload.py
5 | gen/
6 | node_modules/
7 | .tmp/
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | notifications:
5 | email: false
6 | sudo: false
7 | before_install:
8 | - npm install -g grunt-cli
9 |
--------------------------------------------------------------------------------
/test/iframe.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Some text
5 |
6 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | // This exposes the ./dist Javascript file for node libraries.
2 | // It also unwraps the main axs package so Audit and other objects are exposed
3 | // directly in the node library
4 |
5 | var library = require('./dist/js/axs_testing'); // eslint-disable-line no-undef
6 |
7 | module.exports = library.axs; // eslint-disable-line no-undef
8 |
--------------------------------------------------------------------------------
/test/js/browser-utils-test.js:
--------------------------------------------------------------------------------
1 | module("matchSelector", {
2 | setup: function () {
3 | this.fixture_ = document.getElementById('qunit-fixture');
4 | this.matching_selector_ = '#qunit-fixture';
5 | this.mismatching_selector_ = '#not-fixture';
6 | }
7 | });
8 | test("nodes are the same", function () {
9 | equal(axs.browserUtils.matchSelector(this.fixture_, this.matching_selector_), true);
10 | });
11 |
12 | test("nodes are different", function () {
13 | equal(axs.browserUtils.matchSelector(this.fixture_, this.mismatching_selector_), false);
14 | });
15 |
--------------------------------------------------------------------------------
/src/js/axs.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.provide('axs');
16 |
--------------------------------------------------------------------------------
/test/audits/human-lang-missing-test.js:
--------------------------------------------------------------------------------
1 | module("Human lang");
2 |
3 | test('Test lang attribute must be present', function(assert) {
4 |
5 | var config = {
6 | scope: document.documentElement,
7 | ruleName: 'humanLangMissing',
8 | expected: axs.constants.AuditResult.FAIL
9 | };
10 |
11 | // Remove the humanLang attribute from the qunit test page.
12 | var htmlElement = document.querySelector('html');
13 |
14 | var htmlLang = htmlElement.lang;
15 | htmlElement.lang = '';
16 |
17 |
18 | htmlElement.lang = 'en-US';
19 |
20 | config.expected = axs.constants.AuditResult.PASS;
21 | assert.runRule(config);
22 |
23 | htmlElement.lang = htmlLang;
24 | });
25 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessibility-developer-tools",
3 | "version": "2.11.0",
4 | "homepage": "https://github.com/GoogleChrome/accessibility-developer-tools",
5 | "authors": [
6 | "Google"
7 | ],
8 | "description": "This is a library of accessibility-related testing and utility code.",
9 | "main": "dist/js/axs_testing.js",
10 | "moduleType": [
11 | "amd",
12 | "globals"
13 | ],
14 | "keywords": [
15 | "accessibility",
16 | "testing",
17 | "WCAG",
18 | "module"
19 | ],
20 | "license": "Apache-2.0",
21 | "ignore": [
22 | "**/.*",
23 | "lib",
24 | "scripts",
25 | "src",
26 | "test",
27 | "tools",
28 | "Gruntfile.js",
29 | "package.json"
30 | ]
31 | }
32 |
--------------------------------------------------------------------------------
/test/audits/page-without-title-test.js:
--------------------------------------------------------------------------------
1 | module("Page titles");
2 |
3 | test("Page titles must be present and non-empty", function(assert) {
4 |
5 | // Remove the title element from the qunit test page.
6 | var title = document.querySelector('title');
7 | if (title && title.parentNode)
8 | title.parentNode.removeChild(title);
9 |
10 | var config = {
11 | scope: document.documentElement,
12 | ruleName: 'pageWithoutTitle',
13 | expected: axs.constants.AuditResult.FAIL
14 | };
15 |
16 | // This one fails because there is no title element.
17 | assert.runRule(config);
18 |
19 | var head = document.querySelector('head');
20 | var blankTitle = document.createElement('title');
21 | head.appendChild(blankTitle);
22 |
23 | // This one fails because the title element is blank.
24 | assert.runRule(config);
25 |
26 | blankTitle.textContent = 'foo';
27 | config.expected = axs.constants.AuditResult.PASS;
28 | assert.runRule(config);
29 |
30 | // Put it back the way it was...
31 | blankTitle.parentNode.replaceChild(title, blankTitle);
32 |
33 | });
--------------------------------------------------------------------------------
/scripts/output_wrapper.txt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright <%= grunt.template.today('yyyy') %> Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | *
16 | * Generated from http://github.com/GoogleChrome/accessibility-developer-tools/tree/<%= grunt.config.get('git-revision') %>
17 | *
18 | * See project README for build steps.
19 | */
20 |
21 | // AUTO-GENERATED CONTENT BELOW: DO NOT EDIT! See above for details.
22 |
23 | var fn = (function() {
24 | %output%
25 | return axs;
26 | });
27 |
28 | // Define AMD module if possible, export globals otherwise.
29 | if (typeof define !== 'undefined' && define.amd) {
30 | define([], fn);
31 | } else {
32 | var axs = fn.call(this);
33 | }
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "accessibility-developer-tools",
3 | "version": "2.12.0",
4 | "repository": {
5 | "type": "git",
6 | "url": "http://github.com/GoogleChrome/accessibility-developer-tools"
7 | },
8 | "devDependencies": {
9 | "bluebird": "^2.9.27",
10 | "eslint-plugin-google-camelcase": "0.0.1",
11 | "google-closure-compiler": "^20161201.0.0",
12 | "grunt": "^0.4.5",
13 | "grunt-bump": "^0.3.1",
14 | "grunt-cli": "^0.1.13",
15 | "grunt-contrib-clean": "^0.6.0",
16 | "grunt-contrib-coffee": "^0.13.0",
17 | "grunt-contrib-copy": "^0.8.0",
18 | "grunt-contrib-qunit": "0.7.0",
19 | "grunt-eslint": "^16.0.0",
20 | "grunt-prompt": "^1.3.0",
21 | "load-grunt-tasks": "^3.2.0",
22 | "superagent": "^1.2.0"
23 | },
24 | "scripts": {
25 | "test": "grunt travis --verbose"
26 | },
27 | "description": "This is a library of accessibility-related testing and utility code.",
28 | "bugs": {
29 | "url": "https://github.com/GoogleChrome/accessibility-developer-tools/issues"
30 | },
31 | "homepage": "https://github.com/GoogleChrome/accessibility-developer-tools",
32 | "main": "main.js",
33 | "directories": {
34 | "test": "test"
35 | },
36 | "keywords": [
37 | "accessibility",
38 | "testing",
39 | "WCAG"
40 | ],
41 | "author": "Google",
42 | "license": "Apache-2.0"
43 | }
44 |
--------------------------------------------------------------------------------
/src/audits/HumanLangMissing.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.constants.Severity');
17 |
18 | axs.AuditRules.addRule({
19 | name: 'humanLangMissing',
20 | heading: 'The web page should have the content\'s human language indicated in the markup',
21 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_html_01',
22 | severity: axs.constants.Severity.WARNING,
23 | relevantElementMatcher: function(element) {
24 | return element instanceof element.ownerDocument.defaultView.HTMLHtmlElement;
25 | },
26 | test: function(scope) {
27 | if (!scope.lang)
28 | return true;
29 | return false;
30 | },
31 | code: 'AX_HTML_01'
32 | });
33 |
--------------------------------------------------------------------------------
/src/audits/TabIndexGreaterThanZero.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 |
19 | axs.AuditRules.addRule({
20 | name: 'tabIndexGreaterThanZero',
21 | heading: 'Avoid positive integer values for tabIndex',
22 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_focus_03',
23 | severity: axs.constants.Severity.WARNING,
24 | relevantElementMatcher: function(element) {
25 | var selector = '[tabindex]';
26 | return axs.browserUtils.matchSelector(element, selector);
27 | },
28 | test: function(element) {
29 | if (element.tabIndex > 0)
30 | return true;
31 | },
32 | code: 'AX_FOCUS_03'
33 | });
34 |
--------------------------------------------------------------------------------
/src/audits/VideoWithoutCaptions.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 |
19 | axs.AuditRules.addRule({
20 | name: 'videoWithoutCaptions',
21 | heading: 'Video elements should use elements to provide captions',
22 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_video_01',
23 | severity: axs.constants.Severity.WARNING,
24 | relevantElementMatcher: function(element) {
25 | return axs.browserUtils.matchSelector(element, 'video');
26 | },
27 | test: function(video) {
28 | var captions = video.querySelectorAll('track[kind=captions]');
29 | return !captions.length;
30 | },
31 | code: 'AX_VIDEO_01'
32 | });
33 |
--------------------------------------------------------------------------------
/src/js/externs/externs.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | /** @param {Element} element */
16 | var getEventListeners = function(element) { };
17 |
18 | /**
19 | * @type {Element}
20 | */
21 | HTMLLabelElement.prototype.control;
22 |
23 | /**
24 | * @type {ShadowRoot}
25 | */
26 | ShadowRoot.prototype.olderShadowRoot;
27 |
28 | /**
29 | * Note: will be deprecated at some point; prefer shadowRoot if it exists.
30 | * @type {HTMLShadowElement}
31 | */
32 | HTMLElement.prototype.webkitShadowRoot;
33 |
34 | /**
35 | * @constructor
36 | * @extends {HTMLElement}
37 | */
38 | function HTMLSlotElement() {}
39 |
40 | /**
41 | * @return {?HTMLSlotElement}
42 | */
43 | Element.prototype.assignedSlot = function() {};
44 |
45 | /**
46 | * @return {?HTMLSlotElement}
47 | */
48 | Text.prototype.assignedSlot = function() {};
49 |
--------------------------------------------------------------------------------
/src/js/BrowserUtils.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.provide('axs.browserUtils');
16 |
17 | /**
18 | * Use Webkit matcher when matches() is not supported.
19 | * Use Firefox matcher when Webkit is not supported.
20 | * Use IE matcher when neither webkit nor Firefox supported.
21 | * @param {Element} element
22 | * @param {string} selector
23 | * @return {boolean} true if the element matches the selector
24 | */
25 | axs.browserUtils.matchSelector = function(element, selector) {
26 | if (element.matches)
27 | return element.matches(selector);
28 | if (element.webkitMatchesSelector)
29 | return element.webkitMatchesSelector(selector);
30 | if (element.mozMatchesSelector)
31 | return element.mozMatchesSelector(selector);
32 | if (element.msMatchesSelector)
33 | return element.msMatchesSelector(selector);
34 | return false;
35 | };
36 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true
4 | },
5 | "globals": {
6 | "goog": false,
7 | "axs": true,
8 | "getEventListeners": true
9 | },
10 | "plugins": ["google-camelcase"],
11 | "rules": {
12 | "brace-style": [2, "1tbs", { "allowSingleLine": true }],
13 | "camelcase": 0,
14 | "comma-style": [2, "last"],
15 | "curly": 0,
16 | "dot-notation": 0,
17 | "eqeqeq": 0,
18 | "google-camelcase/google-camelcase": 1,
19 | "indent": 1,
20 | "key-spacing": 1,
21 | "new-cap": 0,
22 | "no-console": 0,
23 | "no-else-return": 1,
24 | "no-fallthrough": 2,
25 | "no-multi-spaces": 1,
26 | "no-param-reassign": 1,
27 | "no-redeclare": 0,
28 | "no-underscore-dangle": 0,
29 | "no-unused-expressions": 0,
30 | "no-use-before-define": 0,
31 | "object-curly-spacing": [1, "always"],
32 | "one-var": [2, "never"],
33 | "quotes": [1, "single", "avoid-escape"],
34 | "quote-props": 0,
35 | "semi": 2,
36 | "semi-spacing": 1,
37 | "space-after-keywords": [2, "always"],
38 | "space-before-blocks": [2, "always"],
39 | "space-before-function-paren": [1, "never"],
40 | "spaced-comment": [2, "always"],
41 | "space-in-parens": [2, "never"],
42 | "space-infix-ops": 1,
43 | "space-unary-ops": 1,
44 | "strict": 0
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/audits/AriaOnReservedElement.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.constants.Severity');
17 | goog.require('axs.properties');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * Based on recommendations in document: http://www.w3.org/TR/aria-in-html/
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'ariaOnReservedElement',
25 | heading: 'This element does not support ARIA roles, states and properties',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_12',
27 | severity: axs.constants.Severity.WARNING,
28 | relevantElementMatcher: function(element) {
29 | return !axs.utils.canTakeAriaAttributes(element);
30 | },
31 | test: function(element) {
32 | return axs.properties.getAriaProperties(element) !== null;
33 | },
34 | code: 'AX_ARIA_12'
35 | });
36 |
--------------------------------------------------------------------------------
/src/audits/AudioWithoutControls.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 |
19 | /**
20 | * This audit flags any audio elements that do not have controls.
21 | */
22 | axs.AuditRules.addRule({
23 | name: 'audioWithoutControls',
24 | heading: 'Audio elements should have controls',
25 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_audio_01',
26 | severity: axs.constants.Severity.WARNING,
27 | relevantElementMatcher: function(element) {
28 | return axs.browserUtils.matchSelector(element, 'audio[autoplay]');
29 | },
30 | test: function(audio) {
31 | var controls = audio.querySelectorAll('[controls]');
32 | return !controls.length && audio.duration > 3;
33 | },
34 | code: 'AX_AUDIO_01'
35 | });
36 |
--------------------------------------------------------------------------------
/src/audits/PageWithoutTitle.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.constants.Severity');
17 |
18 | axs.AuditRules.addRule({
19 | name: 'pageWithoutTitle',
20 | heading: 'The web page should have a title that describes topic or purpose',
21 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_title_01',
22 | severity: axs.constants.Severity.WARNING,
23 | relevantElementMatcher: function(element) {
24 | return element.tagName.toLowerCase() == 'html';
25 | },
26 | test: function(scope) {
27 | var head = scope.querySelector('head');
28 | if (!head)
29 | return true;
30 | var title = head.querySelector('title');
31 | if (!title)
32 | return true;
33 | return !title.textContent;
34 | },
35 | code: 'AX_TITLE_01'
36 | });
37 |
--------------------------------------------------------------------------------
/src/audits/BadAriaRole.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * This audit checks the `role` attribute to ensure it contains a valid, non-abstract ARIA role.
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'badAriaRole',
25 | heading: 'Elements with ARIA roles must use a valid, non-abstract ARIA role',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_01',
27 | severity: axs.constants.Severity.SEVERE,
28 | relevantElementMatcher: function(element) {
29 | return axs.browserUtils.matchSelector(element, '[role]');
30 | },
31 | test: function(element) {
32 | var roles = axs.utils.getRoles(element);
33 | return !roles.valid;
34 | },
35 | code: 'AX_ARIA_01'
36 | });
37 |
--------------------------------------------------------------------------------
/src/audits/MultipleLabelableElementsPerLabel.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.utils');
18 |
19 | axs.AuditRules.addRule({
20 | name: 'multipleLabelableElementsPerLabel',
21 | heading: 'A label element may not have labelable descendants other than its labeled control.',
22 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#-ax_text_03--labels-should-only-contain-one-labelable-element',
23 | severity: axs.constants.Severity.SEVERE,
24 | relevantElementMatcher: function(element) {
25 | return axs.browserUtils.matchSelector(element, 'label');
26 | },
27 | test: function(scope) {
28 | var controls = scope.querySelectorAll(axs.utils.LABELABLE_ELEMENTS_SELECTOR);
29 | if (controls.length > 1)
30 | return true;
31 | },
32 | code: 'AX_TEXT_03'
33 | });
34 |
--------------------------------------------------------------------------------
/src/audits/LowContrast.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.constants.Severity');
17 | goog.require('axs.properties');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * Text elements should have a reasonable contrast ratio
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'lowContrastElements',
25 | heading: 'Text elements should have a reasonable contrast ratio',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_color_01',
27 | severity: axs.constants.Severity.WARNING,
28 | relevantElementMatcher: function(element, flags) {
29 | return !flags.disabled && axs.properties.hasDirectTextDescendant(element);
30 | },
31 | test: function(element) {
32 | var style = window.getComputedStyle(element, null);
33 | var contrastRatio =
34 | axs.utils.getContrastRatioForElementWithComputedStyle(style, element);
35 | return (contrastRatio && axs.utils.isLowContrast(contrastRatio, style));
36 | },
37 | code: 'AX_COLOR_01'
38 | });
39 |
--------------------------------------------------------------------------------
/src/audits/RoleTooltipRequiresDescribedBy.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * 'Elements with role=tooltip should have at least one other element with aria-describedby referring to them.'
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'roleTooltipRequiresDescribedby',
25 | heading: 'Elements with role=tooltip should have a corresponding element with aria-describedby',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_02',
27 | severity: axs.constants.Severity.SEVERE,
28 | relevantElementMatcher: function(element, flags) {
29 | return axs.browserUtils.matchSelector(element, '[role=tooltip]') && !flags.hidden;
30 | },
31 | test: function(element) {
32 | var referrers = axs.utils.getAriaIdReferrers(element, 'aria-describedby');
33 | return !referrers || referrers.length === 0;
34 | },
35 | code: 'AX_TOOLTIP_01'
36 | });
37 |
--------------------------------------------------------------------------------
/test/gui-browser.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test Accessibility Extension
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/audits/MainRoleOnInappropriateElement.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.properties');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * role=main should only appear on significant elements
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'mainRoleOnInappropriateElement',
25 | heading: 'role=main should only appear on significant elements',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_05',
27 | severity: axs.constants.Severity.WARNING,
28 | relevantElementMatcher: function(element) {
29 | return axs.browserUtils.matchSelector(element, '[role~=main]');
30 | },
31 | test: function(element) {
32 | if (axs.utils.isInlineElement(element))
33 | return true;
34 | var computedTextContent = axs.properties.getTextFromDescendantContent(element);
35 | if (!computedTextContent || computedTextContent.length < 50)
36 | return true;
37 |
38 | return false;
39 | },
40 | code: 'AX_ARIA_05'
41 | });
42 |
--------------------------------------------------------------------------------
/test/testUtils.js:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | QUnit.extend(QUnit.assert, {
15 | runRule: function(config, message) {
16 | var ruleName = config.ruleName;
17 | var auditConfig = new axs.AuditConfiguration({
18 | auditRulesToRun: [ruleName],
19 | scope: config.scope || document.getElementById('qunit-fixture'),
20 | walkDom: false // In QUnit tests we never need to walk the entire DOM
21 | });
22 | if (config.ignoreSelectors)
23 | auditConfig.ignoreSelectors(ruleName, config.ignoreSelectors);
24 | var actual = axs.Audit.run(auditConfig);
25 | this.equal(actual.length, 1, 'Only one rule should have run');
26 | var result = actual[0];
27 | this.equal(result.rule.name, ruleName, 'The correct rule should have run');
28 | if (message)
29 | this.equal(result.result, config.expected, message);
30 | else
31 | this.equal(result.result, config.expected);
32 | if (config.elements)
33 | this.deepEqual(result.elements, config.elements, 'The correct number of elements should be included');
34 | }
35 | });
36 |
--------------------------------------------------------------------------------
/test/audits/tab-index-greater-than-zero-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module("tabIndex values");
16 |
17 | test("tabIndex is 0 or -1 passes the audit", function(assert) {
18 | var fixture = document.getElementById('qunit-fixture');
19 |
20 | var listItem = document.createElement("li");
21 | var heading = document.createElement("h2");
22 | fixture.appendChild(listItem);
23 | fixture.appendChild(heading);
24 | listItem.tabIndex = -1;
25 | heading.tabIndex = 0;
26 |
27 | var config = {
28 | ruleName: 'tabIndexGreaterThanZero',
29 | expected: axs.constants.AuditResult.PASS,
30 | elements: []
31 | };
32 | assert.runRule(config);
33 | });
34 |
35 | test("tabIndex with a positive integer fails the audit", function(assert) {
36 | var fixture = document.getElementById('qunit-fixture');
37 |
38 | var listItem = document.createElement("li");
39 | fixture.appendChild(listItem);
40 | listItem.tabIndex = 1;
41 |
42 | var config = {
43 | ruleName: 'tabIndexGreaterThanZero',
44 | expected: axs.constants.AuditResult.FAIL,
45 | elements: [listItem]
46 | };
47 | assert.runRule(config);
48 | });
49 |
--------------------------------------------------------------------------------
/src/audits/NonExistentAriaRelatedElement.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * Attributes which refer to other elements by ID should refer to elements which exist in the DOM'.
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'nonExistentRelatedElement',
25 | heading: 'Attributes which refer to other elements by ID should refer to elements which exist in the DOM',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_html_03',
27 | severity: axs.constants.Severity.SEVERE,
28 | opt_requires: {
29 | idRefs: true
30 | },
31 | relevantElementMatcher: function(element, flags) {
32 | return flags.idrefs.length > 0;
33 | },
34 | test: function(element, flags) {
35 | var idRefs = flags.idrefs;
36 | var missing = idRefs.some(function(id) {
37 | var refElement = document.getElementById(id);
38 | return !refElement;
39 | });
40 | return missing;
41 | },
42 | code: 'AX_HTML_03'
43 | });
44 |
--------------------------------------------------------------------------------
/src/audits/ImageWithoutAltText.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.properties');
19 | goog.require('axs.utils');
20 |
21 | axs.AuditRules.addRule({
22 | name: 'imagesWithoutAltText',
23 | heading: 'Images should have a text alternative or presentational role',
24 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_text_02',
25 | severity: axs.constants.Severity.WARNING,
26 | relevantElementMatcher: function(element, flags) {
27 | return axs.browserUtils.matchSelector(element, 'img') && !flags.hidden;
28 | },
29 | test: function(image) {
30 | var imageIsPresentational = (image.hasAttribute('alt') && image.alt == '') || image.getAttribute('role') == 'presentation';
31 | if (imageIsPresentational)
32 | return false;
33 | var textAlternatives = {};
34 | axs.properties.findTextAlternatives(image, textAlternatives);
35 | var numTextAlternatives = Object.keys(textAlternatives).length;
36 | if (numTextAlternatives == 0)
37 | return true;
38 | return false;
39 | },
40 | code: 'AX_TEXT_02'
41 | });
42 |
--------------------------------------------------------------------------------
/tools/runner/audit.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | var page = require('webpage').create(),
16 | system = require('system'),
17 | url;
18 |
19 | // disabling so we can get the document root from iframes (http -> https)
20 | page.settings.webSecurityEnabled = false;
21 |
22 | if (system.args.length !== 2) {
23 | console.log('Usage: phantomjs audit.js URL');
24 | phantom.exit();
25 | } else {
26 | url = system.args[1];
27 | page.open(url, function (status) {
28 | if (status !== 'success') {
29 | console.log('Failed to load the page at ' +
30 | url +
31 | ". Status was: " +
32 | status
33 | );
34 | phantom.exit();
35 | } else {
36 | page.evaluate(function() {
37 | // if target website has an AMD loader, we need to make sure
38 | // that window.axs is still available
39 | if (typeof define !== 'undefined' && define.amd) {
40 | define.amd = false;
41 | }
42 | });
43 | page.injectJs('../../dist/js/axs_testing.js');
44 | var report = page.evaluate(function() {
45 | var results = axs.Audit.run();
46 | return axs.Audit.createReport(results);
47 | });
48 | console.log(report);
49 | phantom.exit();
50 | }
51 | });
52 | }
53 |
--------------------------------------------------------------------------------
/test/audits/multiple-labelable-elements-per-label-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module("multipleLabelableElementsPerLabel");
16 |
17 | test("one labelable element within a label tag", function(assert) {
18 | var fixture = document.getElementById("qunit-fixture");
19 | var label = document.createElement("label");
20 | var input = document.createElement("input");
21 |
22 | fixture.appendChild(label);
23 | label.appendChild(input);
24 |
25 | var config = {
26 | ruleName: "multipleLabelableElementsPerLabel",
27 | expected: axs.constants.AuditResult.PASS,
28 | elements: []
29 | };
30 | assert.runRule(config, "passes the audit with no matching elements");
31 | });
32 |
33 | test("multiple labelable elements within a label tag", function(assert) {
34 | var fixture = document.getElementById("qunit-fixture");
35 | var label = document.createElement("label");
36 | var input1 = document.createElement("input");
37 | var input2 = document.createElement("input");
38 |
39 | fixture.appendChild(label);
40 | label.appendChild(input1);
41 | label.appendChild(input2);
42 |
43 | var config = {
44 | ruleName: "multipleLabelableElementsPerLabel",
45 | expected: axs.constants.AuditResult.FAIL,
46 | elements: [label]
47 | };
48 | assert.runRule(config, "fails the audit on that label");
49 | });
50 |
--------------------------------------------------------------------------------
/src/audits/UnfocusableElementsWithOnClick.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.constants.Severity');
17 | goog.require('axs.utils');
18 |
19 | axs.AuditRules.addRule({
20 | name: 'unfocusableElementsWithOnClick',
21 | heading: 'Elements with onclick handlers must be focusable',
22 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_focus_02',
23 | severity: axs.constants.Severity.WARNING,
24 | opt_requires: {
25 | consoleAPI: true
26 | },
27 | relevantElementMatcher: function(element, flags) {
28 | // element.ownerDocument may not be current document if it is in an iframe
29 | if (element instanceof element.ownerDocument.defaultView.HTMLBodyElement) {
30 | return false;
31 | }
32 | if (flags.hidden) {
33 | return false;
34 | }
35 | var eventListeners = getEventListeners(element);
36 | if ('click' in eventListeners) {
37 | return true;
38 | }
39 | return false;
40 | },
41 | test: function(element) {
42 | return !element.hasAttribute('tabindex') &&
43 | !axs.utils.isElementImplicitlyFocusable(element) &&
44 | !element.disabled;
45 | },
46 | code: 'AX_FOCUS_02'
47 | });
48 |
--------------------------------------------------------------------------------
/src/audits/MeaningfulBackgroundImage.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.constants.Severity');
17 | goog.require('axs.utils');
18 |
19 | /**
20 | * Meaningful images should not be used in element backgrounds.
21 | */
22 | axs.AuditRules.addRule({
23 | name: 'elementsWithMeaningfulBackgroundImage',
24 | severity: axs.constants.Severity.WARNING,
25 | relevantElementMatcher: function(element, flags) {
26 | return !flags.hidden;
27 | },
28 | heading: 'Meaningful images should not be used in element backgrounds',
29 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_image_01',
30 | test: function(el) {
31 | if (el.textContent && el.textContent.length > 0) {
32 | return false;
33 | }
34 | var style = window.getComputedStyle(el, null);
35 | var bgImage = style.backgroundImage;
36 | if (!bgImage || bgImage === 'undefined' || bgImage === 'none' ||
37 | bgImage.indexOf('url') != 0) {
38 | return false;
39 | }
40 | var width = parseInt(style.width, 10);
41 | var height = parseInt(style.height, 10);
42 | // TODO(bobbrose): could also check for background repeat and position.
43 | return width < 150 && height < 150;
44 | },
45 | code: 'AX_IMAGE_01'
46 | });
47 |
--------------------------------------------------------------------------------
/src/audits/BadAriaAttributeValue.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * This audit checks the values of ARIA states and properties to ensure they are valid.
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'badAriaAttributeValue',
25 | heading: 'ARIA state and property values must be valid',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_04',
27 | severity: axs.constants.Severity.SEVERE,
28 | relevantElementMatcher: function(element) {
29 | var selector = axs.utils.getSelectorForAriaProperties(axs.constants.ARIA_PROPERTIES);
30 | return axs.browserUtils.matchSelector(element, selector);
31 | },
32 | test: function(element) {
33 | for (var property in axs.constants.ARIA_PROPERTIES) {
34 | var ariaProperty = 'aria-' + property;
35 | if (!element.hasAttribute(ariaProperty))
36 | continue;
37 | var propertyValueText = element.getAttribute(ariaProperty);
38 | var propertyValue = axs.utils.getAriaPropertyValue(ariaProperty, propertyValueText, element);
39 | if (!propertyValue.valid)
40 | return true;
41 | }
42 | return false;
43 | },
44 | code: 'AX_ARIA_04'
45 | });
46 |
--------------------------------------------------------------------------------
/src/audits/MultipleAriaOwners.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * An element's ID must not be present in more that one aria-owns attribute at any time.
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'multipleAriaOwners',
25 | heading: 'An element\'s ID must not be present in more that one aria-owns attribute at any time',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_07',
27 | severity: axs.constants.Severity.WARNING,
28 | relevantElementMatcher: function(element) {
29 | /*
30 | * While technically we could instead match elements with ID attribute
31 | * if there are no [aria-owns] elements then this rule is not relevant.
32 | * The fact that the element which will end up having an error is not
33 | * one of these elements is OK.
34 | */
35 | return axs.browserUtils.matchSelector(element, '[aria-owns]');
36 | },
37 | test: function(element) {
38 | var attr = 'aria-owns';
39 | var ownedElements = axs.utils.getIdReferents(attr, element);
40 | return ownedElements.some(function(ownedElement) {
41 | var owners = axs.utils.getAriaIdReferrers(ownedElement, attr);
42 | return (owners.length > 1);
43 | });
44 | },
45 | code: 'AX_ARIA_07'
46 | });
47 |
--------------------------------------------------------------------------------
/src/audits/AriaOwnsDescendant.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * This test checks that aria-owns does not reference an element that is already owned implicitly.
22 | */
23 | axs.AuditRules.addRule({
24 | // TODO(RickSBrown): check for elements that try to 'aria-own' an ancestor;
25 | // Also: own self does not make sense. Perhaps any IDREF pointing to itself is bad?
26 | // Perhaps even extend this beyond ARIA (e.g. label for itself). Have to change return code?
27 | // Also: other "bad hierarchy" tests - e.g. active-descendant owning a non-descendant...
28 | name: 'ariaOwnsDescendant',
29 | heading: 'aria-owns should not be used if ownership is implicit in the DOM',
30 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_06',
31 | severity: axs.constants.Severity.WARNING,
32 | relevantElementMatcher: function(element) {
33 | return axs.browserUtils.matchSelector(element, '[aria-owns]');
34 | },
35 | test: function(element) {
36 | var attr = 'aria-owns';
37 | var ownedElements = axs.utils.getIdReferents(attr, element);
38 | return ownedElements.some(function(ownedElement) {
39 | return (element.compareDocumentPosition(ownedElement) & Node.DOCUMENT_POSITION_CONTAINED_BY);
40 | });
41 | },
42 | code: 'AX_ARIA_06'
43 | });
44 |
--------------------------------------------------------------------------------
/test/audits/bad-aria-role-test.js:
--------------------------------------------------------------------------------
1 | module('BadAriaRole');
2 |
3 | test('No elements === no problems.', function(assert) {
4 | var config = {
5 | ruleName: 'badAriaRole',
6 | expected: axs.constants.AuditResult.NA
7 | };
8 | assert.runRule(config);
9 | });
10 |
11 | test('No roles === no problems.', function(assert) {
12 | // Setup fixture
13 | var fixture = document.getElementById('qunit-fixture');
14 | for (var i = 0; i < 10; i++)
15 | fixture.appendChild(document.createElement('div'));
16 |
17 | var config = {
18 | ruleName: 'badAriaRole',
19 | expected: axs.constants.AuditResult.NA
20 | };
21 | assert.runRule(config);
22 | });
23 |
24 | test('Good role === no problems.', function(assert) {
25 | // Setup fixture
26 | var fixture = document.getElementById('qunit-fixture');
27 | for (var r in axs.constants.ARIA_ROLES) {
28 | if (axs.constants.ARIA_ROLES.hasOwnProperty(r) && !axs.constants.ARIA_ROLES[r]['abstract']) {
29 | var div = document.createElement('div');
30 | div.setAttribute('role', r);
31 | fixture.appendChild(div);
32 | }
33 | }
34 |
35 | var config = {
36 | ruleName: 'badAriaRole',
37 | expected: axs.constants.AuditResult.PASS,
38 | elements: []
39 | };
40 | assert.runRule(config);
41 | });
42 |
43 | test('Bad role == problem', function(assert) {
44 | // Setup fixture
45 | var fixture = document.getElementById('qunit-fixture');
46 | var div = document.createElement('div');
47 | div.setAttribute('role', 'not-an-aria-role');
48 | fixture.appendChild(div);
49 |
50 | var config = {
51 | ruleName: 'badAriaRole',
52 | expected: axs.constants.AuditResult.FAIL,
53 | elements: [div]
54 | };
55 | assert.runRule(config);
56 | });
57 |
58 | test('Abstract role == problem', function(assert) {
59 | // Setup fixture
60 | var fixture = document.getElementById('qunit-fixture');
61 | var div = document.createElement('div');
62 | div.setAttribute('role', 'input');
63 | fixture.appendChild(div);
64 |
65 | var config = {
66 | ruleName: 'badAriaRole',
67 | expected: axs.constants.AuditResult.FAIL,
68 | elements: [div]
69 | };
70 | assert.runRule(config);
71 | });
72 |
--------------------------------------------------------------------------------
/src/audits/RequiredAriaAttributeMissing.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants');
18 | goog.require('axs.properties');
19 | goog.require('axs.utils');
20 |
21 | axs.AuditRules.addRule({
22 | name: 'requiredAriaAttributeMissing',
23 | heading: 'Elements with ARIA roles must have all required attributes for that role',
24 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_03',
25 | severity: axs.constants.Severity.SEVERE,
26 | relevantElementMatcher: function(element) {
27 | return axs.browserUtils.matchSelector(element, '[role]');
28 | },
29 | test: function(element) {
30 | var roles = axs.utils.getRoles(element);
31 | if (!roles.valid)
32 | return false; // That's a different error.
33 | for (var i = 0; i < roles.roles.length; i++) {
34 | var role = roles.roles[i];
35 | var requiredProperties = role.details.requiredPropertiesSet;
36 | for (var property in requiredProperties) {
37 | var propertyKey = property.replace(/^aria-/, '');
38 | var propertyDetails = axs.constants.ARIA_PROPERTIES[propertyKey];
39 | if ('defaultValue' in propertyDetails)
40 | continue;
41 | if (!element.hasAttribute(property)) {
42 | var nativelySupported = axs.properties.getNativelySupportedAttributes(element);
43 | if (nativelySupported.indexOf(property) < 0) {
44 | return true;
45 | }
46 | }
47 | }
48 | }
49 | },
50 | code: 'AX_ARIA_03'
51 | });
52 |
--------------------------------------------------------------------------------
/test/audits/bad-aria-attribute-value-test.js:
--------------------------------------------------------------------------------
1 | module('BadAriaAttributeValue');
2 |
3 | test('Empty idref value is ok', function(assert) {
4 | var fixture = document.getElementById('qunit-fixture');
5 | var div = document.createElement('div');
6 | fixture.appendChild(div);
7 | div.setAttribute('aria-activedescendant', '');
8 | var config = {
9 | ruleName: 'badAriaAttributeValue',
10 | expected: axs.constants.AuditResult.PASS,
11 | elements: []
12 | };
13 | assert.runRule(config);
14 | });
15 |
16 | test('Bad number value doesn\'t cause crash', function(assert) {
17 | var fixture = document.getElementById('qunit-fixture');
18 | var div = document.createElement('div');
19 | fixture.appendChild(div);
20 | div.setAttribute('aria-valuenow', 'foo');
21 | var config = {
22 | ruleName: 'badAriaAttributeValue',
23 | expected: axs.constants.AuditResult.FAIL,
24 | elements: [div]
25 | };
26 | assert.runRule(config);
27 | });
28 |
29 | test('Good number value is good', function(assert) {
30 | var fixture = document.getElementById('qunit-fixture');
31 | var div = document.createElement('div');
32 | fixture.appendChild(div);
33 | div.setAttribute('aria-valuenow', '10');
34 | var config = {
35 | ruleName: 'badAriaAttributeValue',
36 | expected: axs.constants.AuditResult.PASS,
37 | elements: []
38 | };
39 | assert.runRule(config);
40 | });
41 |
42 | test('Good decimal number value is good', function(assert) {
43 | var fixture = document.getElementById('qunit-fixture');
44 | var div = document.createElement('div');
45 | fixture.appendChild(div);
46 | div.setAttribute('aria-valuenow', '0.5');
47 | var config = {
48 | ruleName: 'badAriaAttributeValue',
49 | expected: axs.constants.AuditResult.PASS,
50 | elements: []
51 | };
52 | assert.runRule(config);
53 | });
54 |
55 | test('Good negative number value is good', function(assert) {
56 | var fixture = document.getElementById('qunit-fixture');
57 | var div = document.createElement('div');
58 | fixture.appendChild(div);
59 | div.setAttribute('aria-valuenow', '-10');
60 | var config = {
61 | ruleName: 'badAriaAttributeValue',
62 | expected: axs.constants.AuditResult.PASS,
63 | elements: []
64 | };
65 | assert.runRule(config);
66 | });
67 |
--------------------------------------------------------------------------------
/test/audits/focusable-element-not-visible-not-aria-hidden-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module('FocusableElementNotVisibleAndNotAriaHidden', {
16 | setup: function() {
17 | var fixture = document.createElement('div');
18 | document.getElementById('qunit-fixture').appendChild(fixture);
19 |
20 | this.fixture_ = fixture;
21 | fixture.style.top = 0;
22 | fixture.style.left = 0;
23 | }
24 | });
25 |
26 | test('a focusable element that is visible passes the audit', function(assert) {
27 | var input = document.createElement('input');
28 |
29 | this.fixture_.appendChild(input);
30 |
31 | var config = {
32 | scope: this.fixture_,
33 | ruleName: 'focusableElementNotVisibleAndNotAriaHidden',
34 | elements: [],
35 | expected: axs.constants.AuditResult.PASS
36 | };
37 | assert.runRule(config);
38 | });
39 |
40 | test('a focusable element that is hidden fails the audit', function(assert) {
41 | var input = document.createElement('input');
42 | input.style.opacity = '0';
43 |
44 | this.fixture_.appendChild(input);
45 |
46 | var config = {
47 | scope: this.fixture_,
48 | ruleName: 'focusableElementNotVisibleAndNotAriaHidden',
49 | elements: [input],
50 | expected: axs.constants.AuditResult.FAIL
51 | };
52 | assert.runRule(config);
53 | });
54 |
55 | test('an element with negative tabindex and empty computed text is ignored', function(assert) {
56 | var emptyDiv = document.createElement('div');
57 | emptyDiv.tabIndex = '-1';
58 | this.fixture_.appendChild(emptyDiv);
59 |
60 | var config = {
61 | scope: this.fixture_,
62 | ruleName: 'focusableElementNotVisibleAndNotAriaHidden',
63 | expected: axs.constants.AuditResult.NA
64 | };
65 | assert.runRule(config);
66 | });
67 |
68 |
--------------------------------------------------------------------------------
/src/audits/UncontrolledTabpanel.js:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 |
19 | (function() {
20 | /**
21 | * Checks if the tabpanel is labeled by a tab
22 | *
23 | * @param {Element} element the tabpanel element
24 | * @returns {boolean} the tabpanel has an aria-labelledby with the id of a tab
25 | */
26 | function labeledByATab(element) {
27 | if (element.hasAttribute('aria-labelledby')) {
28 | var labelingElements = document.querySelectorAll('#' + element.getAttribute('aria-labelledby'));
29 | return labelingElements.length === 1 && labelingElements[0].getAttribute('role') === 'tab';
30 | }
31 | return false;
32 | }
33 |
34 | /**
35 | * Checks if the tabpanel is controlled by a tab
36 | * @param {Element} element the tabpanel element
37 | * @returns {*|boolean}
38 | */
39 | function controlledByATab(element) {
40 | var controlledBy = document.querySelectorAll('[role="tab"][aria-controls="' + element.id + '"]')
41 | return element.id && (controlledBy.length === 1);
42 | }
43 |
44 | // This rule addresses the suggested relationship between a tabpanel and a tab here:
45 | // http://www.w3.org/TR/wai-aria/roles#tabpanel
46 | axs.AuditRules.addRule({
47 | name: "uncontrolledTabpanel",
48 | heading: "A tabpanel should be related to a tab via aria-controls or aria-labelledby",
49 | url: "https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_13",
50 | severity: axs.constants.Severity.WARNING,
51 | relevantElementMatcher: function(element) {
52 | return axs.browserUtils.matchSelector(element, '[role="tabpanel"]');
53 | },
54 | test: function(element) {
55 | return !(controlledByATab(element) || labeledByATab(element));
56 | },
57 | code: 'AX_ARIA_13'
58 | });
59 | })();
60 |
--------------------------------------------------------------------------------
/src/audits/FocusableElementNotVisibleAndNotAriaHidden.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.dom');
19 | goog.require('axs.utils');
20 |
21 | /**
22 | * This audit checks for elements that are focusable but invisible or obscured by another element.
23 | */
24 | axs.AuditRules.addRule({
25 | name: 'focusableElementNotVisibleAndNotAriaHidden',
26 | heading: 'These elements are focusable but either invisible or obscured by another element',
27 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_focus_01',
28 | severity: axs.constants.Severity.WARNING,
29 | relevantElementMatcher: function(element) {
30 | var isFocusable = axs.browserUtils.matchSelector(
31 | element, axs.utils.FOCUSABLE_ELEMENTS_SELECTOR);
32 | if (!isFocusable)
33 | return false;
34 | if (element.tabIndex >= 0)
35 | return true;
36 | // Ignore elements which have negative tabindex and an ancestor with a
37 | // widget role, since they can be accessed neither with tab nor with
38 | // a screen reader
39 | for (var parent = axs.dom.parentElement(element); parent != null;
40 | parent = axs.dom.parentElement(parent)) {
41 | if (axs.utils.elementIsAriaWidget(parent))
42 | return false;
43 | }
44 | // Ignore elements which have a negative tabindex and no text content,
45 | // as they will be skipped by assistive technology
46 | var textAlternatives = axs.properties.findTextAlternatives(element, {});
47 | if (textAlternatives === null || textAlternatives.trim() === '')
48 | return false;
49 |
50 | return true;
51 |
52 | },
53 | test: function(element, flags) {
54 | if (flags.hidden)
55 | return false;
56 | element.focus();
57 | return !axs.utils.elementIsVisible(element);
58 | },
59 | code: 'AX_FOCUS_01'
60 | });
61 |
--------------------------------------------------------------------------------
/src/audits/BadAriaAttribute.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.constants');
17 |
18 | (function() {
19 | 'use strict';
20 | // Over many iterations significant performance gain not re-instantiating regex
21 | var ARIA_ATTR_RE = /^aria\-/;
22 |
23 | /**
24 | * This test basically looks for unknown attributes that start with 'aria-'.
25 | *
26 | * It is a warning because it is probably not "illegal" to use an expando that starts
27 | * with 'aria-', just a generally bad idea. Right?
28 | *
29 | * It will catch common typos like "aria-labeledby" and uncommon ones, like "aria-helicopter" :)
30 | *
31 | * @type {axs.AuditRule.Spec}
32 | */
33 | var badAriaAttribute = {
34 | name: 'badAriaAttribute',
35 | heading: 'This element has an invalid ARIA attribute',
36 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_11',
37 | severity: axs.constants.Severity.WARNING,
38 | relevantElementMatcher: function(element) {
39 | var attributes = element.attributes;
40 | for (var i = 0, len = attributes.length; i < len; i++) {
41 | if (ARIA_ATTR_RE.test(attributes[i].name)) {
42 | return true;
43 | }
44 | }
45 | return false;
46 | },
47 | test: function(element) {
48 | var attributes = element.attributes;
49 | for (var i = 0, len = attributes.length; i < len; i++) {
50 | var attributeName = attributes[i].name;
51 | if (ARIA_ATTR_RE.test(attributeName)) {
52 | var lookupName = attributeName.replace(ARIA_ATTR_RE, '');
53 | if (!axs.constants.ARIA_PROPERTIES.hasOwnProperty(lookupName)) {
54 | return true;
55 | }
56 | }
57 | }
58 | return false;
59 | },
60 | code: 'AX_ARIA_11'
61 | };
62 | axs.AuditRules.addRule(badAriaAttribute);
63 | })();
64 |
--------------------------------------------------------------------------------
/scripts/gh_repo.coffee:
--------------------------------------------------------------------------------
1 | request = require 'superagent'
2 | Promise = require 'bluebird'
3 |
4 | # Small utility class to interact with the Github v3 releases API.
5 | module.exports = class GHRepo
6 | constructor: (@config = {}) ->
7 | @baseUrl = "https://api.github.com/repos/#{@config.repo}"
8 |
9 | _buildRequest: (req) ->
10 | req
11 | .auth @config.username, @config.password
12 | .set 'Accept', 'application/vnd.github.v3'
13 | .set 'User-Agent', 'grunt'
14 |
15 | log: -> console.log.apply console, arguments
16 |
17 | getReleaseByTagName: (tag) ->
18 | # GET /repos/:owner/:repo/releases/tags/:tag
19 | new Promise (resolve, reject) =>
20 | @log 'GET', "#{@baseUrl}/releases/tags/#{tag}"
21 | @_buildRequest(request.get "#{@baseUrl}/releases/tags/#{tag}")
22 | .end (err, res) ->
23 | return resolve() if res.statusCode is 404
24 | return reject(err) if err?
25 | return reject("Request failed") if res.statusCode isnt 200
26 | resolve res.body
27 |
28 | getReleases: (tag) ->
29 | # GET /repos/:owner/:repo/releases
30 | new Promise (resolve, reject) =>
31 | @log 'GET', "#{@baseUrl}/releases"
32 | @_buildRequest(request.get "#{@baseUrl}/releases")
33 | .end (err, res) ->
34 | return resolve() if res.statusCode is 404
35 | return reject(err) if err?
36 | return reject("Request failed") if res.statusCode isnt 200
37 | resolve res.body
38 |
39 | updateRelease: (release, payload) ->
40 | # PATCH /repos/:owner/:repo/releases/:id
41 | new Promise (resolve, reject) =>
42 | @log 'PATCH', "#{@baseUrl}/releases/#{release.id}"
43 | @_buildRequest(request.patch "#{@baseUrl}/releases/#{release.id}")
44 | .send payload
45 | .end (err, res) ->
46 | return reject(err) if err?
47 | return reject("Request failed") if res.statusCode isnt 200
48 | resolve res.body
49 |
50 | createRelease: (payload) ->
51 | # POST /repos/:owner/:repo/releases
52 | new Promise (resolve, reject) =>
53 | @log 'POST', "#{@baseUrl}/releases"
54 | @_buildRequest(request.post "#{@baseUrl}/releases")
55 | .send payload
56 | .end (err, res) ->
57 | return reject(err) if err?
58 | return reject("Request failed") if res.statusCode isnt 201
59 | resolve res.body
60 |
61 | getReleaseByName: (name) ->
62 | new Promise (resolve, reject) =>
63 | @getReleases().then (releases = []) ->
64 | for release in releases
65 | return resolve(release) if release.name is name
66 |
67 | return resolve()
68 | .catch (err) ->
69 | reject "Unable to fetch project releases."
70 |
--------------------------------------------------------------------------------
/src/audits/DuplicateId.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.utils');
19 |
20 | /**
21 | * This audit checks for duplicate IDs in the DOM.
22 | */
23 | axs.AuditRules.addRule({
24 | name: 'duplicateId',
25 | heading: 'Any ID referred to via an IDREF must be unique in the DOM',
26 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_html_02',
27 | severity: axs.constants.Severity.SEVERE,
28 | opt_requires: {
29 | idRefs: true
30 | },
31 | /**
32 | * @this {axs.AuditRule}
33 | */
34 | relevantElementMatcher: function(element, flags) {
35 | if (flags.idrefs.length && !flags.hidden) {
36 | this.relatedElements.push({
37 | element: element,
38 | flags: flags
39 | });
40 | }
41 | if (element.hasAttribute('id')) {
42 | return true;
43 | }
44 | return false;
45 | },
46 | /**
47 | * @this {axs.AuditRule}
48 | */
49 | isRelevant: function(element, flags) {
50 | var id = element.id;
51 | var level = flags.level;
52 | return this.relatedElements.some(function(related) {
53 | var idrefs = related.flags.idrefs;
54 | return related.flags.level === level && idrefs.indexOf(id) >= 0;
55 | });
56 | },
57 | test: function(element) {
58 | /*
59 | * Checks for duplicate IDs within the context of this element.
60 | * This is not a pure a11y check however IDREF attributes in ARIA and HTML (label 'for', td 'headers)
61 | * depend on IDs being correctly implemented.
62 | * Because this audit is noisy (in practice duplicate IDs are not unusual and often harmless)
63 | * we limit this audit to IDs which are actually referred to via any IDREF attribute.
64 | */
65 | var id = element.id;
66 | var selector = '[id=\'' + id.replace(/'/g, '\\\'') + '\']';
67 | var elementsWithId = element.ownerDocument.querySelectorAll(selector);
68 | return (elementsWithId.length > 1);
69 | },
70 | code: 'AX_HTML_02'
71 | });
72 |
--------------------------------------------------------------------------------
/test/audits/focusable-element-not-visible-not-aria-hidden-test-browser.js:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module('FocusableElementNotVisibleAndNotAriaHiddenBrowser', {
16 | setup: function() {
17 | var fixture = document.createElement('div');
18 | document.getElementById('qunit-fixture').appendChild(fixture);
19 |
20 | this.fixture_ = fixture;
21 | document.getElementById('qunit-fixture').style.top = 0;
22 | document.getElementById('qunit-fixture').style.left = 0;
23 | },
24 | teardown: function() {
25 | document.getElementById('qunit-fixture').style.removeProperty('top');
26 | document.getElementById('qunit-fixture').style.removeProperty('left');
27 | }
28 | });
29 |
30 | test('a focusable element that is hidden but shown on focus passes the audit', function(assert) {
31 | var style = document.createElement('style');
32 | var skipLink = document.createElement('a');
33 |
34 | skipLink.href = '#main';
35 | skipLink.id = 'skip';
36 | skipLink.textContent = 'Skip to content';
37 |
38 | style.appendChild(document.createTextNode("a#skip { position:fixed; top: -1000px; left: -1000px }" +
39 | "a#skip:focus, a#skip:active { top: 10px; left: 10px }"));
40 | this.fixture_.appendChild(style);
41 | this.fixture_.appendChild(skipLink);
42 |
43 | var config = {
44 | scope: this.fixture_,
45 | ruleName: 'focusableElementNotVisibleAndNotAriaHidden',
46 | expected: axs.constants.AuditResult.PASS,
47 | elements: []
48 | };
49 | assert.runRule(config);
50 | });
51 |
52 | test('a focusable element inside of Shadow DOM is not "obscured" by the shadow host', function(assert) {
53 | var host = this.fixture_.appendChild(document.createElement("div"));
54 | host.id = 'host';
55 | if (host.createShadowRoot) {
56 | var root = host.createShadowRoot();
57 | var shadowLink = root.appendChild(document.createElement('a'));
58 | shadowLink.href = '#main';
59 | shadowLink.id = 'shadowLink';
60 | shadowLink.textContent = 'Skip to content';
61 |
62 | var config = {
63 | scope: this.fixture_,
64 | ruleName: 'focusableElementNotVisibleAndNotAriaHidden',
65 | expected: axs.constants.AuditResult.PASS,
66 | elements: []
67 | };
68 | assert.runRule(config);
69 | deepEqual(axs.utils.overlappingElements(shadowLink), []);
70 | } else {
71 | console.warn("Test platform does not support shadow DOM");
72 | ok(true);
73 | }
74 | });
75 |
--------------------------------------------------------------------------------
/test/js/color-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module("Contrast Ratio", {
16 | setup: function () {
17 | var fixture = document.createElement('div');
18 | document.getElementById('qunit-fixture').appendChild(fixture);
19 | this.fixture_ = fixture;
20 | this.black_ = {"red": 0, "green": 0, "blue": 0, "alpha": 1};
21 | this.white_ = {"red": 255, "green": 255, "blue": 255, "alpha": 1};
22 | }
23 | });
24 | test("Black and white.", function () {
25 | equal(axs.color.calculateContrastRatio(this.white_, this.black_), 21);
26 | equal(axs.color.calculateContrastRatio(this.black_, this.white_), 21);
27 | });
28 | test("Same color === no contrast.", function () {
29 | equal(axs.color.calculateContrastRatio(this.white_, this.white_), 1);
30 | equal(axs.color.calculateContrastRatio(this.black_, this.black_), 1);
31 | });
32 | test("Transparent foreground === no contrast.", function () {
33 | equal(axs.color.calculateContrastRatio({"red": 0, "green": 0, "blue": 0, "alpha": 0}, this.white_), 1);
34 | });
35 |
36 | module("parseColor");
37 | test("parses alpha values correctly", function() {
38 | var colorString = 'rgba(255, 255, 255, .47)';
39 | var color = axs.color.parseColor(colorString);
40 | equal(color.red, 255);
41 | equal(color.blue, 255);
42 | equal(color.green, 255);
43 | equal(color.alpha, .47);
44 | });
45 |
46 | test("handles rgba transparent value correctly", function() {
47 | var colorString = 'rgba(0, 0, 0, 0)';
48 | var color = axs.color.parseColor(colorString);
49 | equal(color.red, 0);
50 | equal(color.blue, 0);
51 | equal(color.green, 0);
52 | equal(color.alpha, 0);
53 | });
54 |
55 | test("handles xbrowser 'transparent' value correctly", function() {
56 | // Firefox, IE11, Project Spartan (MS Edge Release Candidate)
57 | // See #180 https://github.com/GoogleChrome/accessibility-developer-tools/issues/180
58 | var colorString = 'transparent';
59 | var color = axs.color.parseColor(colorString);
60 | equal(color.red, 0);
61 | equal(color.blue, 0);
62 | equal(color.green, 0);
63 | equal(color.alpha, 0);
64 | });
65 |
66 | module("suggestColors");
67 | test("suggests correct grey values", function() {
68 | var white = new axs.color.Color(255, 255, 255, 1)
69 | var desiredContrastRatios = { AA: 4.5, AAA: 7.0 };
70 | var suggestions = axs.color.suggestColors(white, white, desiredContrastRatios);
71 | deepEqual(suggestions, { AA: { bg: "#ffffff", contrast: "4.54", fg: "#767676" },
72 | AAA: { bg: "#ffffff", contrast: "7.00", fg: "#595959" } });
73 | });
74 |
--------------------------------------------------------------------------------
/src/audits/ControlsWithoutLabel.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 | goog.require('axs.dom');
19 | goog.require('axs.utils');
20 |
21 | /**
22 | * This audit checks that form controls and media elements have labels.
23 | */
24 | axs.AuditRules.addRule({
25 | name: 'controlsWithoutLabel',
26 | heading: 'Controls and media elements should have labels',
27 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_text_01',
28 | severity: axs.constants.Severity.SEVERE,
29 | relevantElementMatcher: function(element) {
30 | var controlsSelector = ['input:not([type="hidden"]):not([disabled])',
31 | 'select:not([disabled])',
32 | 'textarea:not([disabled])',
33 | 'button:not([disabled])',
34 | 'video:not([disabled])'].join(', ');
35 | var isControl = axs.browserUtils.matchSelector(element, controlsSelector);
36 | if (!isControl || element.getAttribute('role') == 'presentation')
37 | return false;
38 | if (element.tabIndex >= 0)
39 | return true;
40 | // Ignore elements which have negative tabindex and an ancestor with a
41 | // widget role, since they can be accessed neither with tab nor with
42 | // a screen reader
43 | for (var parent = axs.dom.parentElement(element); parent != null;
44 | parent = axs.dom.parentElement(parent)) {
45 | if (axs.utils.elementIsAriaWidget(parent))
46 | return false;
47 | }
48 | return true;
49 | },
50 | test: function(control, flags) {
51 | if (flags.hidden)
52 | return false;
53 | if (control.tagName.toLowerCase() == 'input' &&
54 | control.type == 'button' &&
55 | control.value.length) {
56 | return false;
57 | }
58 | if (control.tagName.toLowerCase() == 'button') {
59 | var innerText = control.textContent.replace(/^\s+|\s+$/g, '');
60 | if (innerText.length)
61 | return false;
62 | }
63 | if (axs.utils.hasLabel(control))
64 | return false;
65 | var textAlternatives = axs.properties.findTextAlternatives(control, {});
66 | if (textAlternatives === null || textAlternatives.trim() === '')
67 | return true;
68 | return false;
69 | },
70 | code: 'AX_TEXT_01',
71 | ruleName: 'Controls and media elements should have labels'
72 | });
73 |
--------------------------------------------------------------------------------
/test/audits/main-role-on-inappropriate-element-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module('MainRoleOnInappropriateElement');
16 |
17 | test('No role=main -> no relevant elements', function(assert) {
18 | var fixture = document.getElementById('qunit-fixture');
19 | var div = document.createElement('div');
20 | fixture.appendChild(div);
21 |
22 | var config = {
23 | ruleName: 'mainRoleOnInappropriateElement',
24 | expected: axs.constants.AuditResult.NA
25 | };
26 | assert.runRule(config);
27 | });
28 |
29 | test('role=main on empty element === fail', function(assert) {
30 | var fixture = document.getElementById('qunit-fixture');
31 | var div = document.createElement('div');
32 | div.setAttribute('role', 'main');
33 | fixture.appendChild(div);
34 |
35 | var config = {
36 | ruleName: 'mainRoleOnInappropriateElement',
37 | expected: axs.constants.AuditResult.FAIL,
38 | elements: [div]
39 | };
40 | assert.runRule(config);
41 | });
42 |
43 | test('role=main on element with textContent < 50 characters === pass', function(assert) {
44 | var fixture = document.getElementById('qunit-fixture');
45 | var div = document.createElement('div');
46 | div.setAttribute('role', 'main');
47 | div.textContent = 'Lorem ipsum dolor sit amet.';
48 | fixture.appendChild(div);
49 |
50 | var config = {
51 | ruleName: 'mainRoleOnInappropriateElement',
52 | expected: axs.constants.AuditResult.FAIL,
53 | elements: [div]
54 | };
55 | assert.runRule(config);
56 | });
57 |
58 | test('role=main on element with textContent >= 50 characters === pass', function(assert) {
59 | var fixture = document.getElementById('qunit-fixture');
60 | var div = document.createElement('div');
61 | div.setAttribute('role', 'main');
62 | div.textContent = 'Lorem ipsum dolor sit amet, consectetur cras amet.';
63 | fixture.appendChild(div);
64 |
65 | var config = {
66 | ruleName: 'mainRoleOnInappropriateElement',
67 | expected: axs.constants.AuditResult.PASS,
68 | elements: []
69 | };
70 | assert.runRule(config);
71 | });
72 |
73 | test('role=main on inline element === fail', function(assert) {
74 | var fixture = document.getElementById('qunit-fixture');
75 | var span = document.createElement('span');
76 | span.setAttribute('role', 'main');
77 | span.textContent = 'Lorem ipsum dolor sit amet, consectetur cras amet.';
78 | fixture.appendChild(span);
79 |
80 | var config = {
81 | ruleName: 'mainRoleOnInappropriateElement',
82 | expected: axs.constants.AuditResult.FAIL,
83 | elements: [span]
84 | };
85 | assert.runRule(config);
86 | });
87 |
--------------------------------------------------------------------------------
/src/audits/AriaRoleNotScoped.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants');
18 | goog.require('axs.dom');
19 | goog.require('axs.utils');
20 |
21 | /**
22 | * This test checks ARIA roles which must be owned by another role.
23 | * For example a role of `tab` can only exist within a `tablist`.
24 | * This ownership can be represented implicitly by DOM hierarchy or explictly through the `aria-owns` attribute.
25 | */
26 | axs.AuditRules.addRule({
27 | name: 'ariaRoleNotScoped',
28 | heading: 'Elements with ARIA roles must be in the correct scope',
29 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_09',
30 | severity: axs.constants.Severity.SEVERE,
31 | relevantElementMatcher: function(element) {
32 | return axs.browserUtils.matchSelector(element, '[role]');
33 | },
34 | test: function(element) {
35 | /*
36 | * Checks that this element is in the required scope for its role.
37 | */
38 | var elementRole = axs.utils.getRoles(element);
39 | if (!elementRole || !elementRole.applied)
40 | return false;
41 | var appliedRole = elementRole.applied;
42 | var ariaRole = appliedRole.details;
43 | var requiredScope = ariaRole['scope'];
44 | if (!requiredScope || requiredScope.length === 0) {
45 | return false;
46 | }
47 | var parent = element;
48 | while (parent = axs.dom.parentElement(parent)) {
49 | var parentRole = axs.utils.getRoles(parent, true);
50 | if (parentRole && parentRole.applied) {
51 | var appliedParentRole = parentRole.applied;
52 | if (requiredScope.indexOf(appliedParentRole.name) >= 0) // if this ancestor role is one of the required roles
53 | return false;
54 | }
55 | }
56 | // If we made it this far then no DOM ancestor has a required scope role.
57 | // Now we need to check if anything aria-owns this element.
58 | var owners = axs.utils.getAriaIdReferrers(element, 'aria-owns'); // there can only be ONE explicit owner but that's a different test
59 | if (owners) {
60 | for (var i = 0; i < owners.length; i++) {
61 | var ownerRole = axs.utils.getRoles(owners[i], true);
62 | if (ownerRole && ownerRole.applied && requiredScope.indexOf(ownerRole.applied.name) >= 0)
63 | return false; // the owner role is one of the required roles
64 | }
65 | }
66 | return true;
67 | },
68 | code: 'AX_ARIA_09'
69 | });
70 |
--------------------------------------------------------------------------------
/src/audits/LinkWithUnclearPurpose.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 |
19 | /**
20 | * The purpose of each link should be clear from the link text.
21 | */
22 | axs.AuditRules.addRule({
23 | name: 'linkWithUnclearPurpose',
24 | heading: 'The purpose of each link should be clear from the link text',
25 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_text_04',
26 | severity: axs.constants.Severity.WARNING,
27 | /**
28 | * @param {Element} element
29 | * @return {boolean}
30 | */
31 | relevantElementMatcher: function(element, flags) {
32 | return axs.browserUtils.matchSelector(element, 'a[href]') && !flags.hidden;
33 | },
34 | /**
35 | * @param {Element} anchor
36 | * @param {Object=} opt_config
37 | * @return {boolean}
38 | */
39 | test: function(anchor, flags, opt_config) {
40 | var config = opt_config || {};
41 | var blacklistPhrases = config['blacklistPhrases'] || [];
42 | var whitespaceRE = /\s+/;
43 | for (var i = 0; i < blacklistPhrases.length; i++) {
44 | // Match the blacklist phrase, case insensitively, as the whole string (allowing for
45 | // punctuation at the end).
46 | // For example, a blacklist phrase of "click here" will match "Click here." and
47 | // "click here..." but not "Click here to learn more about trout fishing".
48 | var phraseREString =
49 | '^\\s*' + blacklistPhrases[i].trim().replace(whitespaceRE, '\\s*') + '\s*[^a-z]$';
50 | var phraseRE = new RegExp(phraseREString, 'i');
51 | if (phraseRE.test(anchor.textContent))
52 | return true;
53 | }
54 |
55 | // Remove punctuation from phrase, then strip out all stopwords. Fail if remaining text is
56 | // all whitespace.
57 | var stopwords = config['stopwords'] ||
58 | ['click', 'tap', 'go', 'here', 'learn', 'more', 'this', 'page', 'link', 'about'];
59 | var filteredText = axs.properties.findTextAlternatives(anchor, {});
60 | if (filteredText === null || filteredText.trim() === '')
61 | return true;
62 | filteredText = filteredText.replace(/[^a-zA-Z ]/g, '');
63 | for (var i = 0; i < stopwords.length; i++) {
64 | var stopwordRE = new RegExp('\\b' + stopwords[i] + '\\b', 'ig');
65 | filteredText = filteredText.replace(stopwordRE, '');
66 | if (filteredText.trim() == '')
67 | return true;
68 | }
69 | return false;
70 | },
71 | code: 'AX_TEXT_04'
72 | });
73 |
--------------------------------------------------------------------------------
/src/audits/UnsupportedAriaAttribute.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants');
18 | goog.require('axs.utils');
19 |
20 | (function() {
21 | 'use strict';
22 | // Over many iterations it makes a significant performance difference not to re-instantiate regex
23 | var ARIA_ATTR_RE = /^aria\-/;
24 | // No need to compute the selector for every element in the DOM.
25 | var selector = axs.utils.getSelectorForAriaProperties(axs.constants.ARIA_PROPERTIES);
26 |
27 | /**
28 | * This test looks for known ARIA states and properties that have been used with a role that does
29 | * not support it.
30 | *
31 | * Severe because people think they are converying information they are not. Right?
32 | *
33 | * @type {axs.AuditRule.Spec}
34 | */
35 | var unsupportedAriaAttribute = {
36 | name: 'unsupportedAriaAttribute',
37 | heading: 'This element has an unsupported ARIA attribute',
38 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_10',
39 | severity: axs.constants.Severity.SEVERE,
40 | relevantElementMatcher: function(element) {
41 | return axs.browserUtils.matchSelector(element, selector);
42 | },
43 | test: function(element) {
44 | // Even though we may not need to look up role, supported etc it's better performance to do it here than in loop
45 | var role = axs.utils.getRoles(element, true);
46 | var supported;
47 | if (role && role.applied) {
48 | supported = /** @type {Object} */ (role.applied.details.propertiesSet);
49 | } else {
50 | // This test ignores the fact that some HTML elements should not take even global attributes.
51 | supported = axs.constants.GLOBAL_PROPERTIES;
52 | }
53 | var attributes = element.attributes;
54 | for (var i = 0, len = attributes.length; i < len; i++) {
55 | var attributeName = attributes[i].name;
56 | if (ARIA_ATTR_RE.test(attributeName)) {
57 | var lookupName = attributeName.replace(ARIA_ATTR_RE, '');
58 | // we're only interested in known aria properties
59 | if (axs.constants.ARIA_PROPERTIES.hasOwnProperty(lookupName) && !(attributeName in supported)) {
60 | return true;
61 | }
62 | }
63 | }
64 | return false;
65 | },
66 | code: 'AX_ARIA_10'
67 | };
68 | axs.AuditRules.addRule(unsupportedAriaAttribute);
69 | })();
70 |
--------------------------------------------------------------------------------
/scripts/parse_aria_schemas.py:
--------------------------------------------------------------------------------
1 | import json
2 | import re
3 | import urllib
4 | import xml.etree.ElementTree as ET
5 |
6 | def parse_attributes():
7 | schema = urllib.urlopen('http://www.w3.org/MarkUp/SCHEMA/aria-attributes-1.xsd')
8 | tree = ET.parse(schema)
9 |
10 | for node in tree.iter():
11 | node.tag = re.sub(r'{.*}', r'', node.tag)
12 |
13 | type_map = {
14 | 'states': 'state',
15 | 'props': 'property'
16 | }
17 | properties = {}
18 | groups = tree.getroot().findall('attributeGroup')
19 | print groups
20 | for group in groups:
21 | print(group.get('name'))
22 | name_match = re.match(r'ARIA\.(\w+)\.attrib', group.get('name'))
23 | if not name_match:
24 | continue
25 | group_type = name_match.group(1)
26 | print group_type
27 | if group_type not in type_map:
28 | continue
29 | type = type_map[group_type]
30 | for child in group:
31 | name = re.sub(r'aria-', r'', child.attrib['name'])
32 | property = {}
33 | property['type'] = type
34 | if 'type' in child.attrib:
35 | valueType = re.sub(r'xs:', r'', child.attrib['type'])
36 | if valueType == 'IDREF':
37 | property['valueType'] = 'idref'
38 | elif valueType == 'IDREFS':
39 | property['valueType'] = 'idref_list'
40 | else:
41 | property['valueType'] = valueType
42 | else:
43 | type_spec = child.findall('simpleType')[0]
44 | restriction_spec = type_spec.findall('restriction')[0]
45 | base = restriction_spec.attrib['base']
46 | if base == 'xs:NMTOKENS':
47 | property['valueType'] = 'token_list'
48 | elif base == 'xs:NMTOKEN':
49 | property['valueType'] = 'token'
50 | else:
51 | raise Exception('Unknown value type: %s' % base)
52 | values = []
53 | for value_type in restriction_spec:
54 | values.append(value_type.get('value'))
55 | property['values'] = values
56 | if 'default' in child.attrib:
57 | property['defaultValue'] = child.attrib['default']
58 | properties[name] = property
59 | return json.dumps(properties, sort_keys=True, indent=4, separators=(',', ': '))
60 |
61 |
62 |
63 | if __name__ == "__main__":
64 | attributes_json = parse_attributes()
65 | constants_file = open('src/js/Constants.js', 'r')
66 | new_constants_file = open('src/js/Constants.new.js', 'w')
67 | in_autogen_block = False
68 | for line in constants_file:
69 | if not in_autogen_block:
70 | new_constants_file.write('%s' % line)
71 | if re.match(r'// BEGIN ARIA_PROPERTIES_AUTOGENERATED', line):
72 | in_autogen_block = True
73 | if re.match(r'// END ARIA_PROPERTIES_AUTOGENERATED', line):
74 | break
75 | new_constants_file.write('/** @type {Object.} */\n')
76 | new_constants_file.write('axs.constants.ARIA_PROPERTIES = %s;\n' % attributes_json)
77 | new_constants_file.write('// END ARIA_PROPERTIES_AUTOGENERATED\n')
78 | for line in constants_file:
79 | new_constants_file.write('%s' % line)
80 |
--------------------------------------------------------------------------------
/src/js/AuditRules.js:
--------------------------------------------------------------------------------
1 | // Copyright 2012 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRule');
16 |
17 | goog.provide('axs.AuditRules');
18 |
19 | (function() {
20 | var auditRulesByName = {};
21 | var auditRulesByCode = {};
22 |
23 | /** @type {Object.} */
24 | axs.AuditRules.specs = {};
25 |
26 | /**
27 | * Instantiates and registers an audit rule.
28 | * If a conflicting rule is already registered then the new rule will not be added.
29 | * @param {axs.AuditRule.Spec} spec The object which defines the AuditRule to add.
30 | * @throws {Error} If the rule duplicates properties that must be unique.
31 | */
32 | axs.AuditRules.addRule = function(spec) {
33 | // create the auditRule before checking props as we can expect the constructor to perform the
34 | // first layer of sanity checking.
35 | var auditRule = new axs.AuditRule(spec);
36 | if (auditRule.code in auditRulesByCode)
37 | throw new Error('Can not add audit rule with same code: "' + auditRule.code + '"');
38 | if (auditRule.name in auditRulesByName)
39 | throw new Error('Can not add audit rule with same name: "' + auditRule.name + '"');
40 | auditRulesByName[auditRule.name] = auditRulesByCode[auditRule.code] = auditRule;
41 | axs.AuditRules.specs[spec.name] = spec;
42 | };
43 |
44 | /**
45 | * Gets the audit rule with the given name.
46 | * @param {string} name The name (or code) of an audit rule.
47 | * @return {axs.AuditRule}
48 | */
49 | axs.AuditRules.getRule = function(name) {
50 | return auditRulesByName[name] || auditRulesByCode[name] || null;
51 | };
52 |
53 | /**
54 | * Gets all registered audit rules.
55 | * @param {boolean=} opt_namesOnly If true then the result will contain only the rule names.
56 | * @return {Array.|Array.}
57 | */
58 | axs.AuditRules.getRules = function(opt_namesOnly) {
59 | var ruleNames = Object.keys(auditRulesByName);
60 | if (opt_namesOnly)
61 | return ruleNames;
62 | return ruleNames.map(function(name) {
63 | return this.getRule(name);
64 | }, axs.AuditRules);
65 | };
66 |
67 | /**
68 | * Gets all registered audit rules which are not excluded by configuration.
69 | * @param {axs.AuditConfiguration} configuration Used to determine ignored rules.
70 | * @return {Array.}
71 | */
72 | axs.AuditRules.getActiveRules = function(configuration) {
73 | var auditRules;
74 | if (configuration.auditRulesToRun && configuration.auditRulesToRun.length > 0) {
75 | auditRules = configuration.auditRulesToRun;
76 | } else {
77 | auditRules = axs.AuditRules.getRules(true);
78 | }
79 | if (configuration.auditRulesToIgnore) {
80 | for (var i = 0; i < configuration.auditRulesToIgnore.length; i++) {
81 | var auditRuleToIgnore = configuration.auditRulesToIgnore[i];
82 | if (auditRules.indexOf(auditRuleToIgnore) < 0)
83 | continue;
84 | auditRules.splice(auditRules.indexOf(auditRuleToIgnore), 1);
85 | }
86 | }
87 | return auditRules.map(axs.AuditRules.getRule);
88 | };
89 | })();
90 |
--------------------------------------------------------------------------------
/src/audits/RequiredOwnedAriaRoleMissing.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants');
18 | goog.require('axs.utils');
19 |
20 | (function() {
21 | /**
22 | * @type {axs.AuditRule.Spec}
23 | */
24 | var spec = {
25 | name: 'requiredOwnedAriaRoleMissing',
26 | heading: 'Elements with ARIA roles must ensure required owned elements are present',
27 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_aria_08',
28 | severity: axs.constants.Severity.SEVERE,
29 | relevantElementMatcher: function(element) {
30 | if (!axs.browserUtils.matchSelector(element, '[role]'))
31 | return false;
32 | var required = getRequired(element);
33 | return required.length > 0;
34 |
35 | },
36 | test: function(element) {
37 | /*
38 | * Checks that this element contains everything it "must contain".
39 | */
40 | var busy = element.getAttribute('aria-busy');
41 | if (busy === 'true') // In future this will lower the severity of the warning instead
42 | return false; // https://github.com/GoogleChrome/accessibility-developer-tools/issues/101
43 |
44 | var required = getRequired(element);
45 | for (var i = required.length - 1; i >= 0; i--) {
46 | var descendants = axs.utils.findDescendantsWithRole(element, required[i]);
47 | if (descendants && descendants.length) { // if we found at least one descendant with a required role
48 | return false;
49 | }
50 | }
51 | // if we get to this point our element has 'required owned elements' but it does not own them implicitly in the DOM
52 | var ownedElements = axs.utils.getIdReferents('aria-owns', element);
53 | for (var i = ownedElements.length - 1; i >= 0; i--) {
54 | var ownedElement = ownedElements[i];
55 | var ownedElementRole = axs.utils.getRoles(ownedElement, true);
56 | if (ownedElementRole && ownedElementRole.applied) {
57 | var appliedRole = ownedElementRole.applied;
58 | for (var j = required.length - 1; j >= 0; j--) {
59 | if (appliedRole.name === required[j]) { // if this explicitly owned element has a required role
60 | return false;
61 | }
62 | }
63 | }
64 | }
65 | return true; // if we made it here then we did not find the required owned elements in the DOM
66 | },
67 | code: 'AX_ARIA_08'
68 | };
69 |
70 | /**
71 | * Get a list of the roles this element must contain, if any, based on its ARIA role.
72 | * @param {Element} element A DOM element.
73 | * @return {Array.} The roles this element must contain.
74 | */
75 | function getRequired(element) {
76 | var elementRole = axs.utils.getRoles(element);
77 | if (!elementRole || !elementRole.applied)
78 | return [];
79 | var appliedRole = elementRole.applied;
80 | if (!appliedRole.valid)
81 | return [];
82 | return appliedRole.details['mustcontain'] || [];
83 | }
84 | axs.AuditRules.addRule(spec);
85 | })();
86 |
--------------------------------------------------------------------------------
/test/js/audit-rules-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | (function(){
16 |
17 | module("axs.AuditRules.getRules");
18 |
19 | function buildDummySpec() {
20 | var dummySpec = {
21 | name: 'dummySpec',
22 | heading: 'This is a dummy spec',
23 | url: '',
24 | severity: axs.constants.Severity.WARNING,
25 | relevantElementMatcher: function() {
26 | throw new Error('This should never be called');
27 | },
28 | test: function() {
29 | throw new Error('This should never be called');
30 | },
31 | code: 'AX_DUMMY_01'
32 | };
33 | return dummySpec;
34 | }
35 |
36 |
37 | test("Get all registered rules", function () {
38 | var rules = axs.AuditRules.getRules();
39 | notEqual(rules.length, 0, 'Nothing to test!');
40 | for (var i = 0; i < rules.length; i++) {
41 | ok(rules[i] instanceof axs.AuditRule);
42 | }
43 | });
44 |
45 | test("Get all registered rule names", function () {
46 | var rules = axs.AuditRules.getRules();
47 | notEqual(rules.length, 0, 'Nothing to test!');
48 | var names = axs.AuditRules.getRules(true);
49 | equal(rules.length, names.length, 'A name for every rule and a rule for every name!');
50 | for (var i = 0; i < rules.length; i++) {
51 | equal(rules[i].name, names[i]);
52 | }
53 | });
54 |
55 | module("axs.AuditRules.getRule");
56 |
57 | test("Attempt to register a rule with a duplicate name", function () {
58 | var rules = axs.AuditRules.getRules();
59 | notEqual(rules.length, 0, 'Nothing to test!');
60 | for (var i = 0; i < rules.length; i++) {
61 | var rule = axs.AuditRules.getRule(rules[0].name);
62 | strictEqual(rules[0], rule);
63 | }
64 | });
65 |
66 | module("axs.AuditRules.addRule");
67 |
68 | test("Attempt to add a rule with a duplicate name", function () {
69 | var rules = axs.AuditRules.getRules();
70 | var ruleCount = rules.length;
71 | notEqual(ruleCount, 0, 'Nothing to test!');
72 | var ruleBefore = axs.AuditRules.getRule(rules[0].name);
73 | var spec = buildDummySpec();
74 | spec.name = ruleBefore.name;
75 | raises(function() {
76 | axs.AuditRules.addRule(spec);
77 | }, "An error should be thrown when trying to add a rule with duplicate name.");
78 | var ruleAfter = axs.AuditRules.getRule(ruleBefore.name);
79 | strictEqual(ruleBefore, ruleAfter, 'addRule should not have added a spec with a duplicate name');
80 | strictEqual(ruleCount, axs.AuditRules.getRules().length, 'rules collection should not have changed');
81 | });
82 |
83 | test("Attempt to add a rule with a duplicate code", function () {
84 | var rules = axs.AuditRules.getRules();
85 | var ruleCount = rules.length;
86 | notEqual(ruleCount, 0, 'Nothing to test!');
87 | var ruleBefore = axs.AuditRules.getRule(rules[0].name);
88 | var spec = buildDummySpec();
89 | spec.code = ruleBefore.code;
90 | raises(function() {
91 | axs.AuditRules.addRule(spec);
92 | }, "An error should be thrown when trying to add a rule with duplicate code.");
93 | var ruleAfter = axs.AuditRules.getRule(ruleBefore.name);
94 | strictEqual(ruleBefore, ruleAfter, 'addRule should not have added a spec with a duplicate code');
95 | strictEqual(ruleCount, axs.AuditRules.getRules().length, 'rules collection should not have changed');
96 | });
97 | })();
98 |
--------------------------------------------------------------------------------
/src/audits/TableHasAppropriateHeaders.js:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.require('axs.AuditRules');
16 | goog.require('axs.browserUtils');
17 | goog.require('axs.constants.Severity');
18 |
19 | (function () {
20 | /**
21 | * Checks for a header row in a table.
22 | *
23 | * @param {NodeList} rows tr elements
24 | * @returns {boolean} Table does not have a complete header row
25 | */
26 | function tableDoesNotHaveHeaderRow(rows) {
27 | var headerRow = rows[0];
28 |
29 | var headerCells = headerRow.children;
30 |
31 | for (var i = 0; i < headerCells.length; i++) {
32 | if (headerCells[i].tagName != 'TH') {
33 | return true;
34 | }
35 | }
36 | return false;
37 | }
38 |
39 | /**
40 | * Checks for a header column in a table.
41 | *
42 | * @param {NodeList} rows tr elements
43 | * @returns {boolean} Table does not have a complete header column
44 | */
45 | function tableDoesNotHaveHeaderColumn(rows) {
46 | for (var i = 0; i < rows.length; i++) {
47 | if (rows[i].children[0].tagName != 'TH') {
48 | return true;
49 | }
50 | }
51 | return false;
52 | }
53 |
54 | /**
55 | * Checks whether a table has grid layout with both row and column headers.
56 | *
57 | * @param {NodeList} rows tr elements
58 | * @returns {boolean} Table does not have a complete grid layout
59 | */
60 | function tableDoesNotHaveGridLayout(rows) {
61 | var headerCells = rows[0].children;
62 |
63 | for (var i = 1; i < headerCells.length; i++) {
64 | if (headerCells[i].tagName != 'TH') {
65 | return true;
66 | }
67 | }
68 |
69 | for (var i = 1; i < rows.length; i++) {
70 | if (rows[i].children[0].tagName != 'TH') {
71 | return true;
72 | }
73 | }
74 | return false;
75 | }
76 |
77 | /**
78 | * Checks whether a table is a layout table.
79 | *
80 | * @returns {boolean} Table is a layout table
81 | */
82 | function isLayoutTable(element) {
83 | if (element.childElementCount == 0) {
84 | return true;
85 | }
86 |
87 | if (element.hasAttribute('role') && element.getAttribute('role') != 'presentation') {
88 | return false;
89 | }
90 |
91 | if (element.getAttribute('role') == 'presentation') {
92 | var tableChildren = element.querySelectorAll('*')
93 |
94 | // layout tables should only contain TR and TD elements
95 | for (var i = 0; i < tableChildren.length; i++) {
96 | if (tableChildren[i].tagName != 'TR' && tableChildren[i].tagName != 'TD') {
97 | return false;
98 | }
99 | }
100 |
101 | return true;
102 | }
103 |
104 | return false;
105 | }
106 |
107 | axs.AuditRules.addRule({
108 | name: 'tableHasAppropriateHeaders',
109 | heading: 'Tables should have appropriate headers',
110 | url: 'https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules#ax_table_01',
111 | severity: axs.constants.Severity.SEVERE,
112 | relevantElementMatcher: function (element) {
113 | return axs.browserUtils.matchSelector(element, 'table') && !isLayoutTable(element) && element.querySelectorAll('tr').length > 0;
114 | },
115 | test: function (element) {
116 | var rows = element.querySelectorAll('tr');
117 |
118 | return tableDoesNotHaveHeaderRow(rows) &&
119 | tableDoesNotHaveHeaderColumn(rows) &&
120 | tableDoesNotHaveGridLayout(rows);
121 | },
122 | code: 'AX_TABLE_01',
123 | });
124 | })();
125 |
--------------------------------------------------------------------------------
/src/js/AuditResults.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | goog.provide('axs.AuditResults');
16 |
17 | /**
18 | * Object to hold results for an Audit run.
19 | * @constructor
20 | */
21 | axs.AuditResults = function() {
22 | /**
23 | * The errors received from the audit run.
24 | * @type {Array.}
25 | * @private
26 | */
27 | this.errors_ = [];
28 |
29 | /**
30 | * The warnings receive from the audit run.
31 | * @type {Array.}
32 | * @private
33 | */
34 | this.warnings_ = [];
35 | };
36 | goog.exportSymbol('axs.AuditResults', axs.AuditResults);
37 |
38 | /**
39 | * Adds an error message to the AuditResults object.
40 | * @param {string} errorMessage The error message to add.
41 | */
42 | axs.AuditResults.prototype.addError = function(errorMessage) {
43 | if (errorMessage != '') {
44 | this.errors_.push(errorMessage);
45 | }
46 |
47 | };
48 | goog.exportProperty(axs.AuditResults.prototype, 'addError',
49 | axs.AuditResults.prototype.addError);
50 |
51 | /**
52 | * Adds a warning message to the AuditResults object.
53 | * @param {string} warningMessage The Warning message to add.
54 | */
55 | axs.AuditResults.prototype.addWarning = function(warningMessage) {
56 | if (warningMessage != '') {
57 | this.warnings_.push(warningMessage);
58 | }
59 |
60 | };
61 | goog.exportProperty(axs.AuditResults.prototype, 'addWarning',
62 | axs.AuditResults.prototype.addWarning);
63 |
64 | /**
65 | * Returns the number of errors in this AuditResults object.
66 | * @return {number} The number of errors in the AuditResults object.
67 | */
68 | axs.AuditResults.prototype.numErrors = function() {
69 | return this.errors_.length;
70 | };
71 | goog.exportProperty(axs.AuditResults.prototype, 'numErrors',
72 | axs.AuditResults.prototype.numErrors);
73 |
74 | /**
75 | * Returns the number of warnings in this AuditResults object.
76 | * @return {number} The number of warnings in the AuditResults object.
77 | */
78 | axs.AuditResults.prototype.numWarnings = function() {
79 | return this.warnings_.length;
80 | };
81 | goog.exportProperty(axs.AuditResults.prototype, 'numWarnings',
82 | axs.AuditResults.prototype.numWarnings);
83 |
84 | /**
85 | * Returns the errors in this AuditResults object.
86 | * @return {Array.} An array of the audit errors.
87 | */
88 | axs.AuditResults.prototype.getErrors = function() {
89 | return this.errors_;
90 | };
91 | goog.exportProperty(axs.AuditResults.prototype, 'getErrors',
92 | axs.AuditResults.prototype.getErrors);
93 |
94 | /**
95 | * Returns the warnings in this AuditResults object.
96 | * @return {Array.} An array of the audit warnings.
97 | */
98 | axs.AuditResults.prototype.getWarnings = function() {
99 | return this.warnings_;
100 | };
101 | goog.exportProperty(axs.AuditResults.prototype, 'getWarnings',
102 | axs.AuditResults.prototype.getWarnings);
103 |
104 | /**
105 | * Returns a string message depicting AuditResults values.
106 | * @return {string} A string representation of the AuditResults object.
107 | */
108 | axs.AuditResults.prototype.toString = function() {
109 | var message = '';
110 | for (var i = 0; i < this.errors_.length; i++) {
111 | if (i == 0) {
112 | message += '\nErrors:\n';
113 | }
114 | var result = this.errors_[i];
115 | message += result + '\n\n';
116 | }
117 | for (var i = 0; i < this.warnings_.length; i++) {
118 | if (i == 0) {
119 | message += '\nWarnings:\n';
120 | }
121 | var result = this.warnings_[i];
122 | message += result + '\n\n';
123 | }
124 | return message;
125 | };
126 | goog.exportProperty(axs.AuditResults.prototype, 'toString',
127 | axs.AuditResults.prototype.toString);
128 |
--------------------------------------------------------------------------------
/test/audits/image-without-alt-text-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | (function() {
16 | module('ImageWithoutAltText');
17 | var RULE_NAME = 'imagesWithoutAltText';
18 |
19 | test('Image with no text alternative', function(assert) {
20 | var fixture = document.getElementById('qunit-fixture');
21 | var img = fixture.appendChild(document.createElement('img'));
22 | img.src = 'smile.jpg';
23 |
24 | var config = {
25 | ruleName: RULE_NAME,
26 | elements: [img],
27 | expected: axs.constants.AuditResult.FAIL
28 | };
29 | assert.runRule(config, 'Image has no text alternative');
30 | });
31 |
32 | test('Image with no text alternative and presentational role', function(assert) {
33 | var fixture = document.getElementById('qunit-fixture');
34 | var img = fixture.appendChild(document.createElement('img'));
35 | img.src = 'smile.jpg';
36 | img.setAttribute('role', 'presentation');
37 |
38 | var config = {
39 | ruleName: RULE_NAME,
40 | elements: [],
41 | expected: axs.constants.AuditResult.PASS
42 | };
43 | assert.runRule(config, 'Image has presentational role');
44 | });
45 |
46 | test('Image with alt text', function(assert) {
47 | var fixture = document.getElementById('qunit-fixture');
48 | var img = fixture.appendChild(document.createElement('img'));
49 | img.src = 'smile.jpg';
50 | img.alt = 'Smile!';
51 |
52 | var config = {
53 | ruleName: RULE_NAME,
54 | elements: [],
55 | expected: axs.constants.AuditResult.PASS
56 | };
57 | assert.runRule(config, 'Image has alt text');
58 | });
59 |
60 | test('Image with empty alt text', function(assert) {
61 | var fixture = document.getElementById('qunit-fixture');
62 | var img = fixture.appendChild(document.createElement('img'));
63 | img.src = 'smile.jpg';
64 | img.alt = '';
65 |
66 | var config = {
67 | ruleName: RULE_NAME,
68 | elements: [],
69 | expected: axs.constants.AuditResult.PASS
70 | };
71 | assert.runRule(config, 'Image has empty alt text');
72 | });
73 |
74 | test('Image with aria label', function(assert) {
75 | var fixture = document.getElementById('qunit-fixture');
76 | var img = fixture.appendChild(document.createElement('img'));
77 | img.src = 'smile.jpg';
78 | img.setAttribute('aria-label', 'Smile!');
79 |
80 | var config = {
81 | ruleName: RULE_NAME,
82 | elements: [],
83 | expected: axs.constants.AuditResult.PASS
84 | };
85 | assert.runRule(config, 'Image has aria label');
86 | });
87 |
88 | test('Image with aria labelledby', function(assert) {
89 | var fixture = document.getElementById('qunit-fixture');
90 | var img = fixture.appendChild(document.createElement('img'));
91 | img.src = 'smile.jpg';
92 | var label = fixture.appendChild(document.createElement('div'));
93 | label.textContent = 'Smile!';
94 | label.id = 'label';
95 | img.setAttribute('aria-labelledby', 'label');
96 |
97 | var config = {
98 | ruleName: RULE_NAME,
99 | elements: [],
100 | expected: axs.constants.AuditResult.PASS
101 | };
102 | assert.runRule(config, 'Image has aria labelledby');
103 | });
104 |
105 | test('Image with title', function(assert) {
106 | var fixture = document.getElementById('qunit-fixture');
107 | var img = fixture.appendChild(document.createElement('img'));
108 | img.src = 'smile.jpg';
109 | img.setAttribute('title', 'Smile!');
110 |
111 | var config = {
112 | ruleName: RULE_NAME,
113 | elements: [],
114 | expected: axs.constants.AuditResult.PASS
115 | };
116 | assert.runRule(config, 'Image has title');
117 | });
118 | })();
119 |
--------------------------------------------------------------------------------
/test/audits/aria-role-not-scoped-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | module('AriaRoleNotScoped');
15 |
16 | test('Scope role present', function(assert) {
17 | var fixture = document.getElementById('qunit-fixture');
18 | var container = fixture.appendChild(document.createElement('div'));
19 | container.setAttribute('role', 'list');
20 | for (var i = 0; i < 4; i++) {
21 | var item = container.appendChild(document.createElement('span'));
22 | item.setAttribute('role', 'listitem');
23 | }
24 |
25 | var config = {
26 | ruleName: 'ariaRoleNotScoped',
27 | expected: axs.constants.AuditResult.PASS,
28 | elements: []
29 | };
30 | assert.runRule(config);
31 | });
32 |
33 | test('Scope role implicitly present', function(assert) {
34 | /*
35 | * The HTML + ARIA here is not necessarily good - it is built to facilitate testing, nothing else.
36 | */
37 | var fixture = document.getElementById('qunit-fixture');
38 | var container = fixture.appendChild(document.createElement('ol'));
39 | for (var i = 0; i < 4; i++) {
40 | var item = container.appendChild(document.createElement('span'));
41 | item.setAttribute('role', 'listitem');
42 | }
43 |
44 | var config = {
45 | ruleName: 'ariaRoleNotScoped',
46 | expected: axs.constants.AuditResult.PASS,
47 | elements: []
48 | };
49 | assert.runRule(config);
50 | });
51 |
52 | test('Scope role present via aria-owns', function(assert) {
53 | var fixture = document.getElementById('qunit-fixture');
54 | var container = fixture.appendChild(document.createElement('div'));
55 | var siblingContainer = fixture.appendChild(document.createElement('div'));
56 | var ids = [];
57 | container.setAttribute('role', 'list');
58 | for (var i = 0; i < 4; i++) {
59 | var id = ids[i] = 'item' + i;
60 | var item = siblingContainer.appendChild(document.createElement('span'));
61 | item.setAttribute('role', 'listitem');
62 | item.setAttribute('id', id);
63 | }
64 | container.setAttribute('aria-owns', ids.join(' '));
65 | equal(container.childNodes.length, 0, 'container should have no child nodes since we\'re checking use of aria-owns'); // paranoid check to ensure the test itself is correct
66 |
67 | var config = {
68 | ruleName: 'ariaRoleNotScoped',
69 | expected: axs.constants.AuditResult.PASS,
70 | elements: []
71 | };
72 | assert.runRule(config);
73 | });
74 |
75 | test('Scope role missing', function(assert) {
76 | var fixture = document.getElementById('qunit-fixture');
77 | var expected = [];
78 | for (var i = 0; i < 4; i++) {
79 | var item = fixture.appendChild(document.createElement('span'));
80 | item.setAttribute('role', 'listitem');
81 | expected.push(item);
82 | }
83 |
84 | var config = {
85 | ruleName: 'ariaRoleNotScoped',
86 | expected: axs.constants.AuditResult.FAIL,
87 | elements: expected
88 | };
89 | assert.runRule(config);
90 | });
91 |
92 | test('Scope role present - tablist', function(assert) {
93 | var fixture = document.getElementById('qunit-fixture');
94 | var container = fixture.appendChild(document.createElement('ul'));
95 | container.setAttribute('role', 'tablist');
96 | for (var i = 0; i < 4; i++) {
97 | var item = container.appendChild(document.createElement('li'));
98 | item.setAttribute('role', 'tab');
99 | }
100 |
101 | var config = {
102 | ruleName: 'ariaRoleNotScoped',
103 | expected: axs.constants.AuditResult.PASS,
104 | elements: []
105 | };
106 | assert.runRule(config);
107 | });
108 |
109 | test('Scope role missing - tab', function(assert) {
110 | var fixture = document.getElementById('qunit-fixture');
111 | var container = fixture.appendChild(document.createElement('ul'));
112 | var expected = [];
113 | for (var i = 0; i < 4; i++) {
114 | var item = container.appendChild(document.createElement('li'));
115 | item.setAttribute('role', 'tab');
116 | expected.push(item);
117 | }
118 |
119 | var config = {
120 | ruleName: 'ariaRoleNotScoped',
121 | expected: axs.constants.AuditResult.FAIL,
122 | elements: expected
123 | };
124 | assert.runRule(config);
125 | });
126 |
--------------------------------------------------------------------------------
/test/js/gui-utils-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2013 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module('Scroll area', {
16 | setup: function() {
17 | var fixture = document.createElement('div');
18 |
19 | fixture.style.top = '0';
20 | fixture.style.left = '0';
21 | fixture.style.overflow = 'scroll';
22 |
23 | document.getElementById('qunit-fixture').appendChild(fixture);
24 | this.fixture_ = fixture;
25 |
26 | document.getElementById('qunit-fixture').style.top = 0;
27 | document.getElementById('qunit-fixture').style.left = 0;
28 | },
29 | teardown: function() {
30 | document.getElementById('qunit-fixture').style.removeProperty('top');
31 | document.getElementById('qunit-fixture').style.removeProperty('left');
32 | }
33 | });
34 | test('Inside scroll area = no problem', function() {
35 | var input = document.createElement('input');
36 | this.fixture_.appendChild(input);
37 |
38 | equal(axs.utils.elementIsOutsideScrollArea(input), false);
39 | });
40 | test('Outside scroll area = bad', function() {
41 | var input = document.createElement('input');
42 | this.fixture_.appendChild(input);
43 | input.style.top = '-1000px';
44 | input.style.left = '-1000px';
45 | input.style.position = 'absolute';
46 | equal(axs.utils.elementIsOutsideScrollArea(input), true);
47 | });
48 | test('In scroll area for element with overflow:auto or overflow:scroll = ok', function() {
49 | var longDiv = document.createElement('div');
50 | this.fixture_.appendChild(longDiv);
51 | longDiv.style.overflow = 'auto';
52 | longDiv.style.position = 'absolute';
53 | longDiv.style.left = '0';
54 | longDiv.style.top = '0';
55 | longDiv.style.height = '1000px';
56 | for (var i = 0; i < 1000; i++) {
57 | var filler = document.createElement('div');
58 | filler.innerText = 'spam';
59 | longDiv.appendChild(filler);
60 | }
61 | var input = document.createElement('input');
62 | longDiv.appendChild(input);
63 | equal(axs.utils.elementIsOutsideScrollArea(input), false);
64 |
65 | longDiv.style.overflow = 'scroll';
66 | equal(axs.utils.elementIsOutsideScrollArea(input), false);
67 | });
68 | test('In scroll area for element but that element is not inside scroll area = bad', function() {
69 | var longDiv = document.createElement('div');
70 | this.fixture_.appendChild(longDiv);
71 | longDiv.style.overflow = 'auto';
72 | longDiv.style.position = 'absolute';
73 | longDiv.style.left = '-10000px';
74 | longDiv.style.top = '-10000px';
75 | longDiv.style.height = '1000px';
76 | longDiv.style.width = '1000px';
77 | for (var i = 0; i < 1000; i++) {
78 | var filler = document.createElement('div');
79 | filler.innerText = 'spam';
80 | longDiv.appendChild(filler);
81 | }
82 | var input = document.createElement('input');
83 | longDiv.appendChild(input);
84 | equal(axs.utils.elementIsOutsideScrollArea(input), true);
85 | });
86 | test('Clipped by element = bad even if inside scroll area', function() {
87 | this.fixture_.innerHTML =
88 | '\n' +
107 | '\n' +
108 | ' This button is offscreen \n' +
109 | ' This button is onscreen but clipped. \n' +
110 | '
';
111 | var button = document.querySelector('.b2');
112 | equal(axs.utils.elementIsOutsideScrollArea(button), true);
113 |
114 | var container = document.querySelector('.container');
115 | container.style.overflow = 'scroll';
116 | equal(axs.utils.elementIsOutsideScrollArea(button), true);
117 |
118 | var container = document.querySelector('.container');
119 | container.style.overflow = 'auto';
120 | equal(axs.utils.elementIsOutsideScrollArea(button), true);
121 |
122 | var container = document.querySelector('.container');
123 | container.style.overflow = 'visible';
124 | equal(axs.utils.elementIsOutsideScrollArea(button), false);
125 | });
126 |
--------------------------------------------------------------------------------
/test/audits/aria-owns-descendant-test.js:
--------------------------------------------------------------------------------
1 | (function() { // scope to avoid leaking helpers and variables to global namespace
2 | module('AriaOwnsDescendant');
3 |
4 | var RULE_NAME = 'ariaOwnsDescendant';
5 |
6 | test('Element owns a sibling', function(assert) {
7 | var fixture = document.getElementById('qunit-fixture');
8 | var owned = fixture.appendChild(document.createElement('div'));
9 | owned.id = 'ownedElement';
10 | var owner = fixture.appendChild(document.createElement('div'));
11 | owner.setAttribute('aria-owns', owned.id);
12 |
13 | var config = {
14 | ruleName: RULE_NAME,
15 | expected: axs.constants.AuditResult.PASS,
16 | elements: []
17 | };
18 | assert.runRule(config);
19 | });
20 |
21 | test('Element owns multiple siblings', function(assert) {
22 | var fixture = document.getElementById('qunit-fixture');
23 | var owned = fixture.appendChild(document.createElement('div'));
24 | owned.id = 'ownedElement';
25 | var owned2 = fixture.appendChild(document.createElement('div'));
26 | owned2.id = 'ownedElement2';
27 | var owner = fixture.appendChild(document.createElement('div'));
28 | owner.setAttribute('aria-owns', owned.id + ' ' + owned2.id);
29 |
30 | var config = {
31 | ruleName: RULE_NAME,
32 | expected: axs.constants.AuditResult.PASS,
33 | elements: []
34 | };
35 | assert.runRule(config);
36 | });
37 |
38 | test('Element owns a descendant', function(assert) {
39 | var fixture = document.getElementById('qunit-fixture');
40 | var owner = fixture.appendChild(document.createElement('div'));
41 | var owned = owner.appendChild(document.createElement('div'));
42 | for (var i = 0; i < 9; i++) // ensure it works on descendants, not just children
43 | owned = owned.appendChild(document.createElement('div'));
44 | owned.id = 'ownedElement';
45 | owner.setAttribute('aria-owns', owned.id);
46 |
47 | var config = {
48 | ruleName: RULE_NAME,
49 | expected: axs.constants.AuditResult.FAIL,
50 | elements: [owner]
51 | };
52 | assert.runRule(config);
53 | });
54 |
55 | test('Element owns multiple descendants', function(assert) {
56 | var fixture = document.getElementById('qunit-fixture');
57 | var owner = fixture.appendChild(document.createElement('div'));
58 | var owned = owner.appendChild(document.createElement('div'));
59 | for (var i = 0; i < 9; i++) // ensure it works on descendants, not just children
60 | owned = owned.appendChild(document.createElement('div'));
61 | owned.id = 'ownedElement';
62 | var owned2 = owner.appendChild(document.createElement('div'));
63 | owned2.id = 'ownedElement2';
64 | owner.setAttribute('aria-owns', owned.id + ' ' + owned2.id);
65 |
66 | var config = {
67 | ruleName: RULE_NAME,
68 | expected: axs.constants.AuditResult.FAIL,
69 | elements: [owner]
70 | };
71 | assert.runRule(config);
72 | });
73 |
74 | test('Element owns one sibling one descendant', function(assert) {
75 | var fixture = document.getElementById('qunit-fixture');
76 | var owner = fixture.appendChild(document.createElement('div'));
77 | var owned = owner.appendChild(document.createElement('div'));
78 | for (var i = 0; i < 9; i++) // ensure it works on descendants, not just children
79 | owned = owned.appendChild(document.createElement('div'));
80 | owned.id = 'ownedElement';
81 | var owned2 = fixture.appendChild(document.createElement('div'));
82 | owned2.id = 'ownedElement2';
83 | owner.setAttribute('aria-owns', owned.id + ' ' + owned2.id);
84 |
85 | var config = {
86 | ruleName: RULE_NAME,
87 | expected: axs.constants.AuditResult.FAIL,
88 | elements: [owner]
89 | };
90 | assert.runRule(config);
91 | });
92 |
93 | test('Using ignoreSelectors - element owns a descendant', function(assert) {
94 | var fixture = document.getElementById('qunit-fixture');
95 | var owner = fixture.appendChild(document.createElement('div'));
96 | var owned = owner.appendChild(document.createElement('div'));
97 | for (var i = 0; i < 9; i++) // ensure it works on descendants, not just children
98 | owned = owned.appendChild(document.createElement('div'));
99 | owned.id = 'ownedElement';
100 | owner.setAttribute('aria-owns', owned.id);
101 |
102 | var config = {
103 | ruleName: RULE_NAME,
104 | expected: axs.constants.AuditResult.NA,
105 | ignoreSelectors: ['#' + (owner.id = 'ownerElement')]
106 | };
107 | assert.runRule(config, 'ignoreSelectors should skip this failing element');
108 | });
109 | })();
110 |
--------------------------------------------------------------------------------
/test/audits/required-owned-aria-role-missing-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | module('RequiredOwnedAriaRoleMissing');
15 |
16 | test('Explicit role on container and required elements all explicitly present', function(assert) {
17 | var fixture = document.getElementById('qunit-fixture');
18 | var container = fixture.appendChild(document.createElement('div'));
19 | container.setAttribute('role', 'list');
20 | for (var i = 0; i < 4; i++) {
21 | var item = container.appendChild(document.createElement('span'));
22 | item.setAttribute('role', 'listitem');
23 | }
24 |
25 | var config = {
26 | ruleName: 'requiredOwnedAriaRoleMissing',
27 | expected: axs.constants.AuditResult.PASS,
28 | elements: []
29 | };
30 | assert.runRule(config);
31 | });
32 |
33 | test('Explicit role on container and required elements all explicitly present via aria-owns', function(assert) {
34 | var fixture = document.getElementById('qunit-fixture');
35 | var container = fixture.appendChild(document.createElement('div'));
36 | var siblingContainer = fixture.appendChild(document.createElement('div'));
37 | var ids = [];
38 | container.setAttribute('role', 'list');
39 | for (var i = 0; i < 4; i++) {
40 | var id = ids[i] = 'item' + i;
41 | var item = siblingContainer.appendChild(document.createElement('span'));
42 | item.setAttribute('role', 'listitem');
43 | item.setAttribute('id', id);
44 | }
45 | container.setAttribute('aria-owns', ids.join(' '));
46 |
47 | equal(container.childNodes.length, 0); // paranoid check to ensure the test itself is correct
48 | var config = {
49 | ruleName: 'requiredOwnedAriaRoleMissing',
50 | expected: axs.constants.AuditResult.PASS,
51 | elements: []
52 | };
53 | assert.runRule(config);
54 | });
55 |
56 | test('Explicit role on container and required elements missing', function(assert) {
57 | var fixture = document.getElementById('qunit-fixture');
58 | var container = fixture.appendChild(document.createElement('div'));
59 | container.setAttribute('role', 'list');
60 |
61 | var config = {
62 | ruleName: 'requiredOwnedAriaRoleMissing',
63 | expected: axs.constants.AuditResult.FAIL,
64 | elements: [container]
65 | };
66 | assert.runRule(config);
67 | });
68 |
69 | test('Explicit role on aria-busy container and required elements missing', function(assert) {
70 | var fixture = document.getElementById('qunit-fixture');
71 | var container = fixture.appendChild(document.createElement('div'));
72 | container.setAttribute('role', 'list');
73 | container.setAttribute('aria-busy', 'true');
74 |
75 | var config = {
76 | ruleName: 'requiredOwnedAriaRoleMissing',
77 | expected: axs.constants.AuditResult.PASS,
78 | elements: []
79 | };
80 | assert.runRule(config);
81 | });
82 |
83 |
84 | test('Explicit role on container and required elements all implicitly present', function(assert) {
85 | var fixture = document.getElementById('qunit-fixture');
86 | var container = fixture.appendChild(document.createElement('ul'));
87 | container.setAttribute('role', 'list'); // This is bad practice (redundant role) but that's a different test
88 | for (var i = 0; i < 4; i++) {
89 | container.appendChild(document.createElement('li'));
90 | }
91 |
92 | var config = {
93 | ruleName: 'requiredOwnedAriaRoleMissing',
94 | expected: axs.constants.AuditResult.PASS,
95 | elements: []
96 | };
97 | assert.runRule(config);
98 | });
99 |
100 | test('No role', function(assert) {
101 | var fixture = document.getElementById('qunit-fixture');
102 | fixture.appendChild(document.createElement('div'));
103 |
104 | var config = {
105 | ruleName: 'requiredOwnedAriaRoleMissing',
106 | expected: axs.constants.AuditResult.NA
107 | };
108 | assert.runRule(config);
109 | });
110 |
111 | test('Role with no required elements', function(assert) {
112 | var fixture = document.getElementById('qunit-fixture');
113 | var container = fixture.appendChild(document.createElement('div'));
114 | container.setAttribute('role', 'checkbox');
115 |
116 | var config = {
117 | ruleName: 'requiredOwnedAriaRoleMissing',
118 | expected: axs.constants.AuditResult.NA
119 | };
120 | assert.runRule(config);
121 | });
122 |
--------------------------------------------------------------------------------
/test/audits/role-tooltip-requires-described-by-test.js:
--------------------------------------------------------------------------------
1 | module('RoleTooltipRequiresDescribedBy');
2 |
3 | test('role tooltip with a corresponding aria-describedby should pass', function(assert) {
4 | var fixture = document.getElementById('qunit-fixture');
5 | var tooltip = document.createElement('div');
6 | var trigger = document.createElement('div');
7 | fixture.appendChild(tooltip);
8 | fixture.appendChild(trigger);
9 | tooltip.setAttribute('role', 'tooltip');
10 | tooltip.setAttribute('id', 'tooltip1');
11 | trigger.setAttribute('aria-describedby', 'tooltip1');
12 |
13 | var config = {
14 | ruleName: 'roleTooltipRequiresDescribedby',
15 | expected: axs.constants.AuditResult.PASS,
16 | elements: []
17 | };
18 | assert.runRule(config);
19 | });
20 |
21 | test('role tooltip with multiple corresponding aria-describedby should pass', function(assert) {
22 | var fixture = document.getElementById('qunit-fixture');
23 | var tooltip = document.createElement('div');
24 | var trigger1 = document.createElement('div');
25 | var trigger2 = document.createElement('div');
26 | fixture.appendChild(tooltip);
27 | fixture.appendChild(trigger1);
28 | fixture.appendChild(trigger2);
29 | tooltip.setAttribute('role', 'tooltip');
30 | tooltip.setAttribute('id', 'tooltip1');
31 | trigger1.setAttribute('aria-describedby', 'tooltip1');
32 | trigger2.setAttribute('aria-describedby', 'tooltip1');
33 |
34 | var config = {
35 | ruleName: 'roleTooltipRequiresDescribedby',
36 | expected: axs.constants.AuditResult.PASS,
37 | elements: []
38 | };
39 | assert.runRule(config);
40 | });
41 |
42 | test('role tooltip without a aria-describedby should fail', function(assert) {
43 | var fixture = document.getElementById('qunit-fixture');
44 | var tooltip = document.createElement('div');
45 | fixture.appendChild(tooltip);
46 | tooltip.setAttribute('role', 'tooltip');
47 | tooltip.setAttribute('id', 'tooltip1');
48 |
49 | var config = {
50 | ruleName: 'roleTooltipRequiresDescribedby',
51 | expected: axs.constants.AuditResult.FAIL,
52 | elements: [tooltip]
53 | };
54 | assert.runRule(config);
55 | });
56 |
57 | test('role tooltip without a corresponding aria-describedby should fail', function(assert) {
58 | var fixture = document.getElementById('qunit-fixture');
59 | var tooltip = document.createElement('div');
60 | var trigger = document.createElement('div');
61 | fixture.appendChild(tooltip);
62 | fixture.appendChild(trigger);
63 | tooltip.setAttribute('role', 'tooltip');
64 | tooltip.setAttribute('id', 'tooltip1');
65 | trigger.setAttribute('aria-describedby', 'tooltip2');
66 |
67 | var config = {
68 | ruleName: 'roleTooltipRequiresDescribedby',
69 | expected: axs.constants.AuditResult.FAIL,
70 | elements: [tooltip]
71 | };
72 | assert.runRule(config);
73 | });
74 |
75 | test('a hidden tooltip without a corresponding aria-describedby should not fail', function(assert) {
76 | var fixture = document.getElementById('qunit-fixture');
77 | var tooltip = document.createElement('div');
78 | var trigger = document.createElement('div');
79 | fixture.appendChild(tooltip);
80 | fixture.appendChild(trigger);
81 | tooltip.setAttribute('aria-hidden', true);
82 | tooltip.setAttribute('role', 'tooltip');
83 | tooltip.setAttribute('id', 'tooltip1');
84 | trigger.setAttribute('aria-describedby', 'tooltip2');
85 |
86 | var config = {
87 | ruleName: 'roleTooltipRequiresDescribedby',
88 | expected: axs.constants.AuditResult.NA
89 | };
90 | assert.runRule(config);
91 | });
92 |
93 | // #269
94 | test('a tooltip without an ID doesn\'t cause an exception', function(assert) {
95 | var fixture = document.getElementById('qunit-fixture');
96 | var tooltip = document.createElement('div');
97 | fixture.appendChild(tooltip);
98 | tooltip.setAttribute('role', 'tooltip');
99 | try {
100 | var config = {
101 | ruleName: 'roleTooltipRequiresDescribedby',
102 | expected: axs.constants.AuditResult.FAIL,
103 | elements: [tooltip]
104 | };
105 | assert.runRule(config);
106 | } catch (e) {
107 | ok(false, 'Running roleTooltipRequiresDescribedby threw an exception: ' + e.message);
108 | }
109 | });
110 |
111 | test('role tooltip with a corresponding describedby of a missing element id should fail', function(assert) {
112 | var fixture = document.getElementById('qunit-fixture');
113 | var tooltip = document.createElement('div');
114 | var trigger = document.createElement('div');
115 | fixture.appendChild(tooltip);
116 | fixture.appendChild(trigger);
117 | tooltip.setAttribute('role', 'tooltip');
118 | trigger.setAttribute('aria-describedby', 'tooltip1');
119 | var config = {
120 | ruleName: 'roleTooltipRequiresDescribedby',
121 | expected: axs.constants.AuditResult.FAIL,
122 | elements: [tooltip]
123 | };
124 | assert.runRule(config);
125 | });
126 |
--------------------------------------------------------------------------------
/test/audits/multiple-aria-owners-test.js:
--------------------------------------------------------------------------------
1 | (function() { // scope to avoid leaking helpers and variables to global namespace
2 | var RULE_NAME = 'multipleAriaOwners';
3 |
4 | module('MultipleAriaOwners');
5 |
6 | /**
7 | * Helper for aria-owns testing:
8 | * - adds owned elements to the fixture
9 | * - creates owner element/s
10 | * - sets aria-owns on owners
11 | * - returns the fixture
12 | * @param {!Array.} ownedIds The ids that will be 'owned'.
13 | * @param {Array.} ownerIds An id for each 'owner' element.
14 | * @param {string=} attributeValue The value of 'aria-owns'
15 | * otherwise the ownedIds will be used.
16 | * @return {!Element} The test container (qunit fixture).
17 | */
18 | function setup(ownedIds, ownerIds, attributeValue) {
19 | var fixture = document.getElementById('qunit-fixture');
20 | var value = attributeValue || ownedIds.join(' ');
21 | ownedIds.forEach(function(id) {
22 | var element = document.createElement('div');
23 | element.id = id;
24 | fixture.appendChild(element);
25 | });
26 | ownerIds = ownerIds || [''];
27 | ownerIds.forEach(function(id) {
28 | var element = document.createElement('div');
29 | if (id) { // could be an empty string, that is legit here
30 | element.id = id;
31 | }
32 | element.setAttribute('aria-owns', value);
33 | fixture.appendChild(element);
34 | });
35 | return fixture;
36 | }
37 |
38 | test('Element owned once only', function(assert) {
39 | var fixture = setup(['theOwned']);
40 | var config = {
41 | scope: fixture,
42 | ruleName: RULE_NAME,
43 | expected: axs.constants.AuditResult.PASS,
44 | elements: []
45 | };
46 | assert.runRule(config);
47 | });
48 |
49 | test('Multiple elements owned once only', function(assert) {
50 | var fixture = setup(['theOwnedElement', 'theOtherOwnedElement']);
51 | var config = {
52 | scope: fixture,
53 | ruleName: RULE_NAME,
54 | expected: axs.constants.AuditResult.PASS,
55 | elements: []
56 | };
57 | assert.runRule(config);
58 | });
59 |
60 | test('Element owned once only but not found in DOM', function(assert) {
61 | var id = 'theOwnedElement';
62 | var fixture = setup([id]);
63 | var element = document.getElementById(id);
64 | element.parentNode.removeChild(element);
65 | var config = {
66 | scope: fixture,
67 | ruleName: RULE_NAME,
68 | expected: axs.constants.AuditResult.PASS,
69 | elements: []
70 | };
71 | assert.runRule(config);
72 | });
73 |
74 | test('Element owned multiple times', function(assert) {
75 | var ownerIds = ['owner1', 'owner2'];
76 | var fixture = setup(['theOwned'], ownerIds);
77 | var elements = ownerIds.map(function(id) {
78 | return document.getElementById(id);
79 | });
80 |
81 | var config = {
82 | scope: fixture,
83 | ruleName: RULE_NAME,
84 | expected: axs.constants.AuditResult.FAIL,
85 | elements: elements
86 | };
87 | assert.runRule(config);
88 | });
89 |
90 | test('Multiple elements owned multiple times', function(assert) {
91 | var ownerIds = ['owner1', 'owner2', 'owner3'];
92 | var fixture = setup(['theOwnedElement', 'theOtherOwnedElement'], ownerIds);
93 | var elements = ownerIds.map(function(id) {
94 | return document.getElementById(id);
95 | });
96 |
97 | var config = {
98 | scope: fixture,
99 | ruleName: RULE_NAME,
100 | expected: axs.constants.AuditResult.FAIL,
101 | elements: elements
102 | };
103 | assert.runRule(config);
104 | });
105 |
106 |
107 | test('Multiple elements one owned multiple times', function(assert) {
108 | var ownerIds = ['owner1', 'owner2'];
109 | var ownedElements = ['theOwnedElement', 'theOtherOwnedElement'];
110 | var fixture = setup(ownedElements, ownerIds, ownedElements[0]);
111 | var elements = ownerIds.map(function(id) {
112 | return document.getElementById(id);
113 | });
114 |
115 | var config = {
116 | scope: fixture,
117 | ruleName: RULE_NAME,
118 | expected: axs.constants.AuditResult.FAIL,
119 | elements: elements
120 | };
121 | assert.runRule(config);
122 | });
123 |
124 | test('Using ignoreSelectors', function(assert) {
125 | var fixture = setup(['theOwned'], ['owner1', 'owner2']);
126 | var ignoreSelectors = ['#owner1', '#owner2'];
127 |
128 | var config = {
129 | ignoreSelectors: ignoreSelectors,
130 | scope: fixture,
131 | ruleName: RULE_NAME,
132 | expected: axs.constants.AuditResult.NA
133 | };
134 | assert.runRule(config);
135 | });
136 | })();
137 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test Accessibility Extension
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/test/audits/non-existent-aria-related-element-test.js:
--------------------------------------------------------------------------------
1 | module('NonExistentRelatedElement');
2 | [
3 | 'aria-activedescendant', // strictly speaking sometests do not apply to this
4 | 'aria-controls',
5 | 'aria-describedby',
6 | 'aria-flowto',
7 | 'aria-labelledby',
8 | 'aria-owns'].forEach(function(testProp) {
9 | test('Element exists, single ' + testProp + ' value', function(assert) {
10 | var fixture = document.getElementById('qunit-fixture');
11 | var referentElement = document.createElement('div');
12 | referentElement.textContent = 'label';
13 | referentElement.id = 'theLabel';
14 | fixture.appendChild(referentElement);
15 |
16 | var refererElement = document.createElement('div');
17 | refererElement.setAttribute(testProp, 'theLabel');
18 | fixture.appendChild(refererElement);
19 |
20 | var config = {
21 | ruleName: 'nonExistentRelatedElement',
22 | expected: axs.constants.AuditResult.PASS,
23 | elements: []
24 | };
25 | assert.runRule(config);
26 | });
27 |
28 | test('Element doesn\'t exist, single ' + testProp + ' value', function(assert) {
29 | var fixture = document.getElementById('qunit-fixture');
30 |
31 | var refererElement = document.createElement('div');
32 | refererElement.setAttribute(testProp, 'notALabel');
33 | fixture.appendChild(refererElement);
34 |
35 | var config = {
36 | ruleName: 'nonExistentRelatedElement',
37 | expected: axs.constants.AuditResult.FAIL,
38 | elements: [refererElement]
39 | };
40 | assert.runRule(config);
41 | });
42 |
43 | test('Element doesn\'t exist, single ' + testProp + ' value with aria-busy', function(assert) {
44 | var fixture = document.getElementById('qunit-fixture');
45 |
46 | var refererElement = document.createElement('div');
47 | refererElement.setAttribute(testProp, 'notALabel');
48 | refererElement.setAttribute('aria-busy', 'true');
49 | fixture.appendChild(refererElement);
50 |
51 | var config = {
52 | ruleName: 'nonExistentRelatedElement',
53 | expected: axs.constants.AuditResult.FAIL,
54 | elements: [refererElement]
55 | };
56 | assert.runRule(config);
57 | });
58 |
59 | test('Element doesn\'t exist, single ' + testProp + ' value with aria-hidden', function(assert) {
60 | var fixture = document.getElementById('qunit-fixture');
61 |
62 | var refererElement = document.createElement('div');
63 | refererElement.setAttribute(testProp, 'notALabel');
64 | refererElement.setAttribute('aria-hidden', 'true');
65 | fixture.appendChild(refererElement);
66 |
67 | var config = {
68 | ruleName: 'nonExistentRelatedElement',
69 | expected: axs.constants.AuditResult.FAIL,
70 | elements: [refererElement]
71 | };
72 | assert.runRule(config);
73 | });
74 |
75 | test('Multiple referent elements exist with ' + testProp, function(assert) {
76 | var fixture = document.getElementById('qunit-fixture');
77 | var referentElement = document.createElement('div');
78 | referentElement.textContent = 'label';
79 | referentElement.id = 'theLabel';
80 | fixture.appendChild(referentElement);
81 |
82 | var referentElement2 = document.createElement('div');
83 | referentElement2.textContent = 'label2';
84 | referentElement2.id = 'theOtherLabel';
85 | fixture.appendChild(referentElement2);
86 |
87 | var refererElement = document.createElement('div');
88 | refererElement.setAttribute(testProp, 'theLabel theOtherLabel');
89 | fixture.appendChild(refererElement);
90 |
91 | var config = {
92 | ruleName: 'nonExistentRelatedElement',
93 | expected: axs.constants.AuditResult.PASS,
94 | elements: []
95 | };
96 | assert.runRule(config);
97 |
98 | });
99 |
100 | test('One element doesn\'t exist, multiple ' + testProp, function(assert) {
101 | var fixture = document.getElementById('qunit-fixture');
102 |
103 | var referentElement = document.createElement('div');
104 | referentElement.textContent = 'label';
105 | referentElement.id = 'theLabel';
106 | fixture.appendChild(referentElement);
107 |
108 | var refererElement = document.createElement('div');
109 | refererElement.setAttribute(testProp, 'theLabel notALabel');
110 | fixture.appendChild(refererElement);
111 |
112 | var config = {
113 | ruleName: 'nonExistentRelatedElement',
114 | expected: axs.constants.AuditResult.FAIL,
115 | elements: [refererElement]
116 | };
117 | assert.runRule(config);
118 | });
119 |
120 | test('Using ignoreSelectors with ' + testProp, function(assert) {
121 | var fixture = document.getElementById('qunit-fixture');
122 |
123 | var referentElement = document.createElement('div');
124 | referentElement.textContent = 'label2';
125 | referentElement.id = 'theLabel2';
126 | fixture.appendChild(referentElement);
127 |
128 | var refererElement = document.createElement('div');
129 | refererElement.id = 'labelledbyElement2';
130 | refererElement.setAttribute(testProp, 'theLabel2 notALabel2');
131 | fixture.appendChild(refererElement);
132 |
133 | var config = {
134 | ruleName: 'nonExistentRelatedElement',
135 | expected: axs.constants.AuditResult.NA,
136 | ignoreSelectors: ['#labelledbyElement2']
137 | };
138 | assert.runRule(config);
139 | });
140 | });
141 |
--------------------------------------------------------------------------------
/test/qunit.css:
--------------------------------------------------------------------------------
1 | /**
2 | * QUnit v1.7.0pre - A JavaScript Unit Testing Framework
3 | *
4 | * http://docs.jquery.com/QUnit
5 | *
6 | * Copyright (c) 2012 John Resig, Jörn Zaefferer
7 | * Dual licensed under the MIT (MIT-LICENSE.txt)
8 | * or GPL (GPL-LICENSE.txt) licenses.
9 | */
10 |
11 | /** Font Family and Sizes */
12 |
13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult {
14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
15 | }
16 |
17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
18 | #qunit-tests { font-size: smaller; }
19 |
20 |
21 | /** Resets */
22 |
23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult {
24 | margin: 0;
25 | padding: 0;
26 | }
27 |
28 |
29 | /** Header */
30 |
31 | #qunit-header {
32 | padding: 0.5em 0 0.5em 1em;
33 |
34 | color: #8699a4;
35 | background-color: #0d3349;
36 |
37 | font-size: 1.5em;
38 | line-height: 1em;
39 | font-weight: normal;
40 |
41 | border-radius: 15px 15px 0 0;
42 | -moz-border-radius: 15px 15px 0 0;
43 | -webkit-border-top-right-radius: 15px;
44 | -webkit-border-top-left-radius: 15px;
45 | }
46 |
47 | #qunit-header a {
48 | text-decoration: none;
49 | color: #c2ccd1;
50 | }
51 |
52 | #qunit-header a:hover,
53 | #qunit-header a:focus {
54 | color: #fff;
55 | }
56 |
57 | #qunit-header label {
58 | display: inline-block;
59 | padding-left: 0.5em;
60 | }
61 |
62 | #qunit-banner {
63 | height: 5px;
64 | }
65 |
66 | #qunit-testrunner-toolbar {
67 | padding: 0.5em 0 0.5em 2em;
68 | color: #5E740B;
69 | background-color: #eee;
70 | }
71 |
72 | #qunit-userAgent {
73 | padding: 0.5em 0 0.5em 2.5em;
74 | background-color: #2b81af;
75 | color: #fff;
76 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
77 | }
78 |
79 |
80 | /** Tests: Pass/Fail */
81 |
82 | #qunit-tests {
83 | list-style-position: inside;
84 | }
85 |
86 | #qunit-tests li {
87 | padding: 0.4em 0.5em 0.4em 2.5em;
88 | border-bottom: 1px solid #fff;
89 | list-style-position: inside;
90 | }
91 |
92 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running {
93 | display: none;
94 | }
95 |
96 | #qunit-tests li strong {
97 | cursor: pointer;
98 | }
99 |
100 | #qunit-tests li a {
101 | padding: 0.5em;
102 | color: #c2ccd1;
103 | text-decoration: none;
104 | }
105 | #qunit-tests li a:hover,
106 | #qunit-tests li a:focus {
107 | color: #000;
108 | }
109 |
110 | #qunit-tests ol {
111 | margin-top: 0.5em;
112 | padding: 0.5em;
113 |
114 | background-color: #fff;
115 |
116 | border-radius: 15px;
117 | -moz-border-radius: 15px;
118 | -webkit-border-radius: 15px;
119 |
120 | box-shadow: inset 0px 2px 13px #999;
121 | -moz-box-shadow: inset 0px 2px 13px #999;
122 | -webkit-box-shadow: inset 0px 2px 13px #999;
123 | }
124 |
125 | #qunit-tests table {
126 | border-collapse: collapse;
127 | margin-top: .2em;
128 | }
129 |
130 | #qunit-tests th {
131 | text-align: right;
132 | vertical-align: top;
133 | padding: 0 .5em 0 0;
134 | }
135 |
136 | #qunit-tests td {
137 | vertical-align: top;
138 | }
139 |
140 | #qunit-tests pre {
141 | margin: 0;
142 | white-space: pre-wrap;
143 | word-wrap: break-word;
144 | }
145 |
146 | #qunit-tests del {
147 | background-color: #e0f2be;
148 | color: #374e0c;
149 | text-decoration: none;
150 | }
151 |
152 | #qunit-tests ins {
153 | background-color: #ffcaca;
154 | color: #500;
155 | text-decoration: none;
156 | }
157 |
158 | /*** Test Counts */
159 |
160 | #qunit-tests b.counts { color: black; }
161 | #qunit-tests b.passed { color: #5E740B; }
162 | #qunit-tests b.failed { color: #710909; }
163 |
164 | #qunit-tests li li {
165 | margin: 0.5em;
166 | padding: 0.4em 0.5em 0.4em 0.5em;
167 | background-color: #fff;
168 | border-bottom: none;
169 | list-style-position: inside;
170 | }
171 |
172 | /*** Passing Styles */
173 |
174 | #qunit-tests li li.pass {
175 | color: #5E740B;
176 | background-color: #fff;
177 | border-left: 26px solid #C6E746;
178 | }
179 |
180 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
181 | #qunit-tests .pass .test-name { color: #366097; }
182 |
183 | #qunit-tests .pass .test-actual,
184 | #qunit-tests .pass .test-expected { color: #999999; }
185 |
186 | #qunit-banner.qunit-pass { background-color: #C6E746; }
187 |
188 | /*** Failing Styles */
189 |
190 | #qunit-tests li li.fail {
191 | color: #710909;
192 | background-color: #fff;
193 | border-left: 26px solid #EE5757;
194 | white-space: pre;
195 | }
196 |
197 | #qunit-tests > li:last-child {
198 | border-radius: 0 0 15px 15px;
199 | -moz-border-radius: 0 0 15px 15px;
200 | -webkit-border-bottom-right-radius: 15px;
201 | -webkit-border-bottom-left-radius: 15px;
202 | }
203 |
204 | #qunit-tests .fail { color: #000000; background-color: #EE5757; }
205 | #qunit-tests .fail .test-name,
206 | #qunit-tests .fail .module-name { color: #000000; }
207 |
208 | #qunit-tests .fail .test-actual { color: #EE5757; }
209 | #qunit-tests .fail .test-expected { color: green; }
210 |
211 | #qunit-banner.qunit-fail { background-color: #EE5757; }
212 |
213 |
214 | /** Result */
215 |
216 | #qunit-testresult {
217 | padding: 0.5em 0.5em 0.5em 2.5em;
218 |
219 | color: #2b81af;
220 | background-color: #D2E0E6;
221 |
222 | border-bottom: 1px solid white;
223 | }
224 | #qunit-testresult .module-name {
225 | font-weight: bold;
226 | }
227 |
228 | /** Fixture */
229 |
230 | #qunit-fixture {
231 | position: absolute;
232 | top: -10000px;
233 | left: -10000px;
234 | width: 1000px;
235 | height: 1000px;
236 | }
237 |
--------------------------------------------------------------------------------
/test/audits/aria-on-reserved-element-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | (function() {
15 | module('AriaOnReservedElement');
16 | var RULE_NAME = 'ariaOnReservedElement';
17 |
18 | test('Non-reserved element with role and aria- attributes', function(assert) {
19 | var fixture = document.getElementById('qunit-fixture');
20 | var widget = fixture.appendChild(document.createElement('div'));
21 | widget.setAttribute('role', 'spinbutton');
22 | widget.setAttribute('aria-hidden', 'false'); // global
23 | widget.setAttribute('aria-required', 'true'); // supported
24 | widget.setAttribute('aria-valuemax', '79'); // required
25 | widget.setAttribute('aria-valuemin', '10'); // required
26 | widget.setAttribute('aria-valuenow', '50'); // required
27 |
28 | var config = {
29 | ruleName: RULE_NAME,
30 | expected: axs.constants.AuditResult.NA
31 | };
32 | assert.runRule(config, 'Non-reserved elements are not applicable to this test');
33 | });
34 |
35 | test('Non-reserved element with role only', function(assert) {
36 | var fixture = document.getElementById('qunit-fixture');
37 | var widget = fixture.appendChild(document.createElement('div'));
38 | widget.setAttribute('role', 'spinbutton');
39 |
40 | var config = {
41 | ruleName: RULE_NAME,
42 | expected: axs.constants.AuditResult.NA
43 | };
44 | assert.runRule(config, 'Non-reserved elements are not applicable to this test');
45 | });
46 |
47 | test('Non-reserved element with aria-attributes only', function(assert) {
48 | var fixture = document.getElementById('qunit-fixture');
49 | var widget = fixture.appendChild(document.createElement('div'));
50 | widget.setAttribute('aria-hidden', 'false'); // global
51 |
52 | var config = {
53 | ruleName: RULE_NAME,
54 | expected: axs.constants.AuditResult.NA
55 | };
56 | assert.runRule(config, 'Non-reserved elements are not applicable to this test');
57 | });
58 |
59 | test('Reserved element with role and aria- attributes', function(assert) {
60 | var fixture = document.getElementById('qunit-fixture');
61 | var widget = fixture.appendChild(document.createElement('meta'));
62 | widget.setAttribute('role', 'spinbutton');
63 | widget.setAttribute('aria-hidden', 'false'); // global
64 | widget.setAttribute('aria-required', 'true'); // supported
65 | widget.setAttribute('aria-valuemax', '79'); // required
66 | widget.setAttribute('aria-valuemin', '10'); // required
67 | widget.setAttribute('aria-valuenow', '50'); // required
68 |
69 | var config = {
70 | ruleName: RULE_NAME,
71 | expected: axs.constants.AuditResult.FAIL,
72 | elements: [widget]
73 | };
74 | assert.runRule(config, 'Reserved elements can\'t take any ARIA attributes.');
75 | });
76 |
77 | test('Reserved element with role only', function(assert) {
78 | var fixture = document.getElementById('qunit-fixture');
79 | var widget = fixture.appendChild(document.createElement('meta'));
80 | widget.setAttribute('role', 'spinbutton');
81 |
82 | var config = {
83 | ruleName: RULE_NAME,
84 | expected: axs.constants.AuditResult.FAIL,
85 | elements: [widget]
86 | };
87 | assert.runRule(config, 'Reserved elements can\'t take any ARIA attributes.');
88 | });
89 |
90 | test('Reserved element with aria-attributes only', function(assert) {
91 | var fixture = document.getElementById('qunit-fixture');
92 | var widget = fixture.appendChild(document.createElement('meta'));
93 | widget.setAttribute('aria-hidden', 'false'); // global
94 |
95 | var config = {
96 | ruleName: RULE_NAME,
97 | expected: axs.constants.AuditResult.FAIL,
98 | elements: [widget]
99 | };
100 | assert.runRule(config, 'Reserved elements can\'t take any ARIA attributes.');
101 | });
102 |
103 | test('Using ignoreSelectors, reserved element with aria-attributes only', function(assert) {
104 | var fixture = document.getElementById('qunit-fixture');
105 | var widget = fixture.appendChild(document.createElement('meta'));
106 | widget.setAttribute('aria-hidden', 'false'); // global
107 |
108 | var config = {
109 | ruleName: RULE_NAME,
110 | expected: axs.constants.AuditResult.NA,
111 | ignoreSelectors: ['#' + (widget.id = 'ignoreMe')]
112 | };
113 | assert.runRule(config, 'ignoreSelectors should skip this failing element');
114 | });
115 |
116 | test('Reserved element with no ARIA attributes', function(assert) {
117 | var fixture = document.getElementById('qunit-fixture');
118 | fixture.appendChild(document.createElement('meta'));
119 |
120 | var config = {
121 | ruleName: RULE_NAME,
122 | expected: axs.constants.AuditResult.PASS,
123 | elements: []
124 | };
125 | assert.runRule(config, 'A reserved element with no ARIA attributes should pass');
126 | });
127 |
128 | })();
129 |
--------------------------------------------------------------------------------
/test/audits/uncontrolled-tabpanel-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2015 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module('UncontrolledTabpanel');
16 |
17 | test('No roles === NA.', function(assert) {
18 | // Setup fixture
19 | var fixture = document.getElementById('qunit-fixture');
20 | for (var i = 0; i < 10; i++)
21 | fixture.appendChild(document.createElement('div'));
22 |
23 | var config = {
24 | ruleName: 'uncontrolledTabpanel',
25 | expected: axs.constants.AuditResult.NA
26 | };
27 | assert.runRule(config);
28 | });
29 |
30 | test('No elements with role tabpanel === NA.', function(assert) {
31 | // Setup fixture
32 | var fixture = document.getElementById('qunit-fixture');
33 | var div = document.createElement('div');
34 | div.setAttribute('role', 'tablist');
35 | fixture.appendChild(div);
36 |
37 | var config = {
38 | ruleName: 'uncontrolledTabpanel',
39 | expected: axs.constants.AuditResult.NA
40 | };
41 | assert.runRule(config);
42 | });
43 |
44 | test('Tabpanel with aria-labelledby === PASS.', function(assert) {
45 | // Setup fixture
46 | var fixture = document.getElementById('qunit-fixture');
47 | var tabList = document.createElement('div');
48 | tabList.setAttribute('role', 'tablist');
49 | fixture.appendChild(tabList);
50 | var tab = document.createElement('div');
51 | tab.setAttribute('role', 'tab');
52 | tab.setAttribute('id', 'tabId');
53 |
54 | tabList.appendChild(tab);
55 | var tabPanel = document.createElement('div');
56 | tabPanel.setAttribute('role', 'tabpanel');
57 | tabPanel.setAttribute('aria-labelledby', 'tabId');
58 | fixture.appendChild(tabPanel);
59 |
60 | var config = {
61 | ruleName: 'uncontrolledTabpanel',
62 | expected: axs.constants.AuditResult.PASS,
63 | elements: []
64 | };
65 | assert.runRule(config);
66 | });
67 |
68 | test('Tabpanel which is controlled via aria-controls on the tab === PASS.', function(assert) {
69 | // Setup fixture
70 | var fixture = document.getElementById('qunit-fixture');
71 | var tabList = document.createElement('div');
72 | tabList.setAttribute('role', 'tablist');
73 | fixture.appendChild(tabList);
74 | var tab = document.createElement('div');
75 | tab.setAttribute('role', 'tab');
76 | tab.setAttribute('aria-controls', 'tabpanelId');
77 |
78 | tabList.appendChild(tab);
79 | var tabPanel = document.createElement('div');
80 | tabPanel.setAttribute('role', 'tabpanel');
81 | tabPanel.setAttribute('id', 'tabpanelId');
82 | fixture.appendChild(tabPanel);
83 |
84 | var config = {
85 | ruleName: 'uncontrolledTabpanel',
86 | expected: axs.constants.AuditResult.PASS,
87 | elements: []
88 | };
89 | assert.runRule(config);
90 | });
91 |
92 | // If tabpanels were added dynamically with JS, then a tab might not always have a tab panel. This
93 | // test ensures that the audit is only checking for a tabpanel without a tab, not a tab without a
94 | // tabpanel.
95 | test('Tabpanel which is controlled via aria-controls on its tab when there is more than one tab === PASS.', function(assert) {
96 | // Setup fixture
97 | var fixture = document.getElementById('qunit-fixture');
98 | var tabList = document.createElement('div');
99 | tabList.setAttribute('role', 'tablist');
100 | fixture.appendChild(tabList);
101 |
102 | var tab1 = document.createElement('div');
103 | tab1.setAttribute('role', 'tab');
104 | tab1.setAttribute('aria-controls', 'tabpanelId');
105 | tabList.appendChild(tab1);
106 |
107 | var tab2 = document.createElement('div');
108 | tab2.setAttribute('role', 'tab');
109 | tabList.appendChild(tab2);
110 |
111 | var tabPanel = document.createElement('div');
112 | tabPanel.setAttribute('role', 'tabpanel');
113 | tabPanel.setAttribute('id', 'tabpanelId');
114 | fixture.appendChild(tabPanel);
115 |
116 | var config = {
117 | ruleName: 'uncontrolledTabpanel',
118 | expected: axs.constants.AuditResult.PASS,
119 | elements: []
120 | };
121 | assert.runRule(config);
122 | });
123 |
124 | test('Tabpanel which is not controlled or labeled by a tab === FAIL.', function(assert) {
125 | // Setup fixture
126 | var fixture = document.getElementById('qunit-fixture');
127 | var tabList = document.createElement('div');
128 | tabList.setAttribute('role', 'tablist');
129 | fixture.appendChild(tabList);
130 | var tab = document.createElement('div');
131 | tab.setAttribute('role', 'tab');
132 |
133 | tabList.appendChild(tab);
134 | var tabPanel = document.createElement('div');
135 | tabPanel.setAttribute('role', 'tabpanel');
136 | fixture.appendChild(tabPanel);
137 |
138 | var config = {
139 | ruleName: 'uncontrolledTabpanel',
140 | expected: axs.constants.AuditResult.FAIL,
141 | elements: [tabPanel]
142 | };
143 | assert.runRule(config);
144 | });
145 |
146 | test('Tabpanel which is labeled by something other than a tab and not controlled by a tab == FAIL.', function(assert) {
147 | // Setup fixture
148 | var fixture = document.getElementById('qunit-fixture');
149 |
150 | var tabPanel = document.createElement('div');
151 | tabPanel.setAttribute('role', 'tabpanel');
152 | tabPanel.setAttribute('aria-labelledby', 'not-a-tab');
153 | fixture.appendChild(tabPanel);
154 |
155 | var notATab = document.createElement('h5');
156 | notATab.setAttribute('id', 'not-a-tab');
157 | fixture.appendChild(notATab);
158 |
159 | var config = {
160 | ruleName: 'uncontrolledTabpanel',
161 | expected: axs.constants.AuditResult.FAIL,
162 | elements: [tabPanel]
163 | };
164 | assert.runRule(config);
165 | });
166 |
--------------------------------------------------------------------------------
/test/audits/link-with-unclear-purpose.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | module('LinkWithUnclearPurpose');
16 |
17 | test('a link with meaningful text is good', function(assert) {
18 | var fixture = document.getElementById('qunit-fixture');
19 | var a = fixture.appendChild(document.createElement('a'));
20 | a.href = '#main';
21 | a.textContent = 'Learn more about trout fishing';
22 |
23 | var config = {
24 | ruleName: 'linkWithUnclearPurpose',
25 | elements: [],
26 | expected: axs.constants.AuditResult.PASS
27 | };
28 | assert.runRule(config);
29 | });
30 |
31 | test('a link with an img with meaningful alt text is good', function(assert) {
32 | var fixture = document.getElementById('qunit-fixture');
33 | var a = fixture.appendChild(document.createElement('a'));
34 | a.href = '#main';
35 | var img = a.appendChild(document.createElement('img'));
36 | img.setAttribute('alt', 'Learn more about trout fishing');
37 |
38 | var config = {
39 | ruleName: 'linkWithUnclearPurpose',
40 | elements: [],
41 | expected: axs.constants.AuditResult.PASS
42 | };
43 | assert.runRule(config);
44 | });
45 |
46 | test('a link with an img with meaningless alt text is bad', function(assert) {
47 | var fixture = document.getElementById('qunit-fixture');
48 | var a = fixture.appendChild(document.createElement('a'));
49 | a.href = '#main';
50 | var img = a.appendChild(document.createElement('img'));
51 | img.setAttribute('alt', 'Click here!');
52 |
53 | var config = {
54 | ruleName: 'linkWithUnclearPurpose',
55 | elements: [a],
56 | expected: axs.constants.AuditResult.FAIL
57 | };
58 | assert.runRule(config);
59 | });
60 |
61 | /*
62 | * This test will need to be reviewed when issue #214 is addressed.
63 | */
64 | test('a link with meaningful aria-label is good', function(assert) {
65 | var fixture = document.getElementById('qunit-fixture');
66 | // Style our link to be visually meaningful with no descendent nodes at all.
67 | fixture.innerHTML = '';
68 | var a = fixture.appendChild(document.createElement('a'));
69 | a.href = '#main';
70 | a.className = 'trout';
71 | a.setAttribute('aria-label', 'Learn more about trout fishing');
72 |
73 | var config = {
74 | ruleName: 'linkWithUnclearPurpose',
75 | elements: [],
76 | expected: axs.constants.AuditResult.PASS
77 | };
78 | assert.runRule(config);
79 | });
80 |
81 | /*
82 | * This test will need to be reviewed when issue #214 is addressed.
83 | */
84 | test('a link with meaningful aria-labelledby is good', function(assert) {
85 | var fixture = document.getElementById('qunit-fixture');
86 | // Style our link to be visually meaningful with no descendent nodes at all.
87 | fixture.innerHTML = '';
88 | var a = fixture.appendChild(document.createElement('a'));
89 | a.href = '#main';
90 | a.className = 'trout';
91 | var label = fixture.appendChild(document.createElement('span'));
92 | label.textContent = 'Learn more about trout fishing';
93 | label.id = "trout" + Date.now();
94 | a.setAttribute('aria-labelledby', label.id);
95 |
96 | var config = {
97 | ruleName: 'linkWithUnclearPurpose',
98 | elements: [],
99 | expected: axs.constants.AuditResult.PASS
100 | };
101 | assert.runRule(config);
102 | });
103 |
104 | test('a link without meaningful text is bad', function(assert) {
105 | var fixture = document.getElementById('qunit-fixture');
106 | var a = fixture.appendChild(document.createElement('a'));
107 | a.href = '#main';
108 |
109 | var config = {
110 | ruleName: 'linkWithUnclearPurpose',
111 | elements: [a],
112 | expected: axs.constants.AuditResult.FAIL
113 | };
114 |
115 | var badLinks = ['click here.', 'Click here!', 'Learn more.', 'this page', 'this link', 'here'];
116 | badLinks.forEach(function(text) {
117 | a.textContent = text;
118 | assert.runRule(config);
119 | });
120 | });
121 |
122 | test('a link with bg image and meaningful aria-label is good', function(assert) {
123 | var fixture = document.getElementById('qunit-fixture');
124 | // Style our link to be visually meaningful with no descendent nodes at all.
125 | fixture.innerHTML = '';
126 | var a = fixture.appendChild(document.createElement('a'));
127 | a.href = '#main';
128 | a.className = 'trout';
129 | a.setAttribute('aria-label', 'Learn more about trout fishing');
130 |
131 | var config = {
132 | ruleName: 'linkWithUnclearPurpose',
133 | elements: [],
134 | expected: axs.constants.AuditResult.PASS
135 | };
136 | assert.runRule(config);
137 | });
138 |
139 | test('a hidden link should not be run against the audit', function(assert) {
140 | var fixture = document.getElementById('qunit-fixture');
141 | var a = fixture.appendChild(document.createElement('a'));
142 | a.hidden = true;
143 | a.href = '#main';
144 | a.textContent = 'Learn more about trout fishing';
145 |
146 | var config = {
147 | ruleName: 'linkWithUnclearPurpose',
148 | expected: axs.constants.AuditResult.NA
149 | };
150 | assert.runRule(config);
151 | });
152 |
153 | test('an anchor tag without href attribute is ignored', function(assert) {
154 | var fixture = document.getElementById('qunit-fixture');
155 | fixture.appendChild(document.createElement('a'));
156 |
157 | var config = {
158 | ruleName: 'linkWithUnclearPurpose',
159 | expected: axs.constants.AuditResult.NA
160 | };
161 | assert.runRule(config);
162 | });
163 |
--------------------------------------------------------------------------------
/scripts/aria_rdf_to_constants.xsl:
--------------------------------------------------------------------------------
1 |
2 |
17 |
29 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | /** @type {Object.<string, Object>} */
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | namefrom
54 |
55 | 0
56 |
57 |
58 |
59 | parent
60 |
61 |
62 |
63 |
64 |
65 |
66 | requiredProperties
67 |
68 |
69 |
70 |
71 | properties
72 |
73 |
74 |
75 |
76 |
77 | mustcontain
78 |
79 |
80 |
81 |
82 |
83 | scope
84 |
85 |
86 |
87 |
88 |
89 | ,
90 |
91 |
92 |
93 |
94 |
95 | 1
96 |
97 |
98 |
99 | "
100 |
101 | "
102 |
103 | ,
104 |
105 |
106 |
107 |
108 | "
109 |
110 | "
111 |
112 | ,
113 |
114 |
115 |
116 |
117 |
118 |
119 | 1
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | "
132 |
133 | ": [
134 |
135 |
136 |
137 |
138 |
139 | ]
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/test/audits/required-aria-attribute-missing-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | (function() {
15 | module('RequiredAriaAttributeMissing');
16 | var RULE_NAME = 'requiredAriaAttributeMissing';
17 | /**
18 | * Input types that take the role 'spinbutton'.
19 | *
20 | * @const
21 | */
22 | var SPINBUTTON_TYPES = ['date', 'datetime', 'datetime-local',
23 | 'month', 'number', 'time', 'week'];
24 |
25 | test('Explicit states required all present', function(assert) {
26 | var fixture = document.getElementById('qunit-fixture');
27 | var widget = fixture.appendChild(document.createElement('div'));
28 | widget.setAttribute('role', 'slider');
29 | widget.setAttribute('aria-valuemax', '79');
30 | widget.setAttribute('aria-valuemin', '10');
31 | widget.setAttribute('aria-valuenow', '50');
32 |
33 | var config = {
34 | ruleName: RULE_NAME,
35 | expected: axs.constants.AuditResult.PASS,
36 | elements: []
37 | };
38 | assert.runRule(config);
39 | });
40 |
41 | test('Explicit states required but none present', function(assert) {
42 | var fixture = document.getElementById('qunit-fixture');
43 | var widget = fixture.appendChild(document.createElement('div'));
44 | widget.setAttribute('role', 'slider');
45 |
46 | var config = {
47 | ruleName: RULE_NAME,
48 | expected: axs.constants.AuditResult.FAIL,
49 | elements: [widget]
50 | };
51 | assert.runRule(config);
52 | });
53 |
54 | test('Explicit states required only supported states present', function(assert) {
55 | var fixture = document.getElementById('qunit-fixture');
56 | var widget = fixture.appendChild(document.createElement('div'));
57 | widget.setAttribute('role', 'slider');
58 | widget.setAttribute('aria-orientation', 'horizontal'); // supported
59 | widget.setAttribute('aria-haspopup', 'false'); // global
60 |
61 | var config = {
62 | ruleName: RULE_NAME,
63 | expected: axs.constants.AuditResult.FAIL,
64 | elements: [widget]
65 | };
66 | assert.runRule(config);
67 | });
68 |
69 | test('Explicit states required, one not present', function(assert) {
70 | var fixture = document.getElementById('qunit-fixture');
71 | var widget = fixture.appendChild(document.createElement('div'));
72 | widget.setAttribute('role', 'slider');
73 | widget.setAttribute('aria-valuemin', '10');
74 | widget.setAttribute('aria-valuenow', '50');
75 |
76 | var config = {
77 | ruleName: RULE_NAME,
78 | expected: axs.constants.AuditResult.FAIL,
79 | elements: [widget]
80 | };
81 | assert.runRule(config);
82 | });
83 |
84 | /*
85 | * Elements with the role scrollbar have an implicit aria-orientation value
86 | * of vertical.
87 | */
88 | test('Explicit states present, aria implicit state present', function(assert) {
89 | var fixture = document.getElementById('qunit-fixture');
90 | var widget = fixture.appendChild(document.createElement('div'));
91 | var widget2 = fixture.appendChild(document.createElement('div'));
92 | widget2.id = 'controlledElement';
93 | widget.setAttribute('role', 'scrollbar');
94 | widget.setAttribute('aria-valuemax', '79');
95 | widget.setAttribute('aria-valuemin', '10');
96 | widget.setAttribute('aria-valuenow', '50');
97 | widget.setAttribute('aria-orientation', 'horizontal');
98 | widget.setAttribute('aria-controls', widget2.id);
99 |
100 | var config = {
101 | ruleName: RULE_NAME,
102 | expected: axs.constants.AuditResult.PASS,
103 | elements: []
104 | };
105 | assert.runRule(config);
106 | });
107 |
108 | /*
109 | * Elements with the role scrollbar have an implicit aria-orientation value
110 | * of vertical.
111 | */
112 | test('Explicit states present, aria implicit state absent', function(assert) {
113 | var fixture = document.getElementById('qunit-fixture');
114 | var widget = fixture.appendChild(document.createElement('div'));
115 | var widget2 = fixture.appendChild(document.createElement('div'));
116 | widget2.id = 'controlledElement';
117 | widget.setAttribute('role', 'scrollbar');
118 | widget.setAttribute('aria-valuemax', '79');
119 | widget.setAttribute('aria-valuemin', '10');
120 | widget.setAttribute('aria-valuenow', '50');
121 | widget.setAttribute('aria-controls', widget2.id);
122 |
123 | var config = {
124 | ruleName: RULE_NAME,
125 | expected: axs.constants.AuditResult.PASS,
126 | elements: []
127 | };
128 | assert.runRule(config);
129 | });
130 |
131 | test('Required states provided implcitly by html', function(assert) {
132 | var fixture = document.getElementById('qunit-fixture');
133 | var widget = fixture.appendChild(document.createElement('input'));
134 | widget.setAttribute('type', 'range');
135 | // setting role is redundant but needs to be ignored by this audit
136 | widget.setAttribute('role', 'slider');
137 |
138 | var config = {
139 | ruleName: RULE_NAME,
140 | expected: axs.constants.AuditResult.PASS,
141 | elements: []
142 | };
143 | assert.runRule(config);
144 | });
145 |
146 | SPINBUTTON_TYPES.forEach(function(type) {
147 | test('Required states provided implcitly by input type ' + type, function(assert) {
148 | var fixture = document.getElementById('qunit-fixture');
149 | var widget = fixture.appendChild(document.createElement('input'));
150 | widget.setAttribute('type', type);
151 | // setting role is redundant but needs to be ignored by this audit
152 | widget.setAttribute('role', 'spinbutton');
153 |
154 | var config = {
155 | ruleName: RULE_NAME,
156 | expected: axs.constants.AuditResult.PASS,
157 | elements: []
158 | };
159 | assert.runRule(config);
160 | });
161 | });
162 |
163 | })();
164 |
--------------------------------------------------------------------------------
/test/audits/bad-aria-attribute-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | (function() {
15 | module('BadAriaAttribute');
16 | var RULE_NAME = 'badAriaAttribute';
17 |
18 | test('Element with role and global, supported and required attributes', function(assert) {
19 | var fixture = document.getElementById('qunit-fixture');
20 | var widget = fixture.appendChild(document.createElement('div'));
21 | widget.setAttribute('role', 'spinbutton');
22 | widget.setAttribute('aria-hidden', 'false'); // global
23 | widget.setAttribute('aria-required', 'true'); // supported
24 | widget.setAttribute('aria-valuemax', '79'); // required
25 | widget.setAttribute('aria-valuemin', '10'); // required
26 | widget.setAttribute('aria-valuenow', '50'); // required
27 |
28 | var config = {
29 | ruleName: RULE_NAME,
30 | expected: axs.constants.AuditResult.PASS,
31 | elements: []
32 | };
33 | assert.runRule(config, 'Test should pass with global, supported and required attributes for role');
34 | });
35 |
36 | /*
37 | * This rule shouldn't care if required and/or supported roles are missing.
38 | */
39 | test('Element with role and global but missing supported and required attributes', function(assert) {
40 | var fixture = document.getElementById('qunit-fixture');
41 | var widget = fixture.appendChild(document.createElement('div'));
42 | widget.setAttribute('role', 'spinbutton');
43 | widget.setAttribute('aria-hidden', 'false'); // global (so the audit will encounter this element)
44 |
45 | var config = {
46 | ruleName: RULE_NAME,
47 | expected: axs.constants.AuditResult.PASS,
48 | elements: []
49 | };
50 | assert.runRule(config, 'This rule shouldn\'t care if required and/or supported roles are missing.');
51 | });
52 |
53 | /*
54 | * This rule shouldn't care if known ARIA attributes are used with the wrong role.
55 | */
56 | test('Element with role and known but unsupported attributes', function(assert) {
57 | var fixture = document.getElementById('qunit-fixture');
58 | var widget = fixture.appendChild(document.createElement('div'));
59 | widget.setAttribute('role', 'group');
60 | widget.setAttribute('aria-required', 'true'); // unsupported
61 | widget.setAttribute('aria-valuemax', '79'); // unsupported
62 | widget.setAttribute('aria-valuemin', '10'); // unsupported
63 | widget.setAttribute('aria-valuenow', '50'); // unsupported
64 |
65 | var config = {
66 | ruleName: RULE_NAME,
67 | expected: axs.constants.AuditResult.PASS,
68 | elements: []
69 | };
70 | assert.runRule(config, 'This rule shouldn\'t care if known ARIA attributes are used with the wrong role.');
71 | });
72 |
73 | /*
74 | * This rule shouldn't care if we put ARIA attributes on elements that shouldn't have them.
75 | */
76 | test('Element with role and global but missing supported and required attributes', function(assert) {
77 | var fixture = document.getElementById('qunit-fixture');
78 | var widget = fixture.appendChild(document.createElement('meta')); // note, a reserved HTML element
79 | widget.setAttribute('role', 'spinbutton');
80 | widget.setAttribute('aria-hidden', 'false'); // global (so the audit will encounter this element)
81 |
82 | var config = {
83 | ruleName: RULE_NAME,
84 | expected: axs.constants.AuditResult.PASS,
85 | elements: []
86 | };
87 | assert.runRule(config, 'This rule shouldn\'t care if we put ARIA attributes on elements that shouldn\'t have them.');
88 | });
89 |
90 | test('Element with a role and unknown aria- attribute', function(assert) {
91 | var fixture = document.getElementById('qunit-fixture');
92 | var widget = fixture.appendChild(document.createElement('div'));
93 | widget.setAttribute('role', 'spinbutton');
94 | widget.setAttribute('aria-labeledby', 'false'); // unknown
95 | widget.setAttribute('aria-hidden', 'false'); // global
96 | widget.setAttribute('aria-required', 'true'); // supported
97 | widget.setAttribute('aria-valuemax', '79'); // required
98 | widget.setAttribute('aria-valuemin', '10'); // required
99 | widget.setAttribute('aria-valuenow', '50'); // required
100 |
101 | var config = {
102 | ruleName: RULE_NAME,
103 | expected: axs.constants.AuditResult.FAIL,
104 | elements: [widget]
105 | };
106 | assert.runRule(config, 'This rule should detect unknown "aria-" attributes on elements with role');
107 | });
108 |
109 | /*
110 | * This rule definitely needs to visit elements with no role attribute.
111 | */
112 | test('Element with no role and unknown aria- attribute', function(assert) {
113 | var fixture = document.getElementById('qunit-fixture');
114 | var widget = fixture.appendChild(document.createElement('div'));
115 | widget.setAttribute('aria-bananapeel', 'oops'); // unknown
116 |
117 | var config = {
118 | ruleName: RULE_NAME,
119 | expected: axs.constants.AuditResult.FAIL,
120 | elements: [widget]
121 | };
122 | assert.runRule(config, 'This rule should detect unknown "aria-" attributes on elements without role');
123 | });
124 |
125 | /*
126 | * This rule can ignore elements with no aria- attributes.
127 | */
128 | test('Element with role but no aria- attributes', function(assert) {
129 | var fixture = document.getElementById('qunit-fixture');
130 | var widget = fixture.appendChild(document.createElement('div'));
131 | widget.setAttribute('role', 'spinbutton');
132 |
133 | var config = {
134 | ruleName: RULE_NAME,
135 | expected: axs.constants.AuditResult.NA
136 | };
137 | assert.runRule(config, 'This rule should ignore elements with no aria- attributes.');
138 | });
139 |
140 | test('Element with no role and some known, some unknown aria- attributes', function(assert) {
141 | var fixture = document.getElementById('qunit-fixture');
142 | var widget = fixture.appendChild(document.createElement('div'));
143 | widget.setAttribute('aria-busy', 'false'); // global
144 | widget.setAttribute('aria-hidden', 'false'); // global
145 | widget.setAttribute('aria-awards', 'true'); // unknown
146 | widget.setAttribute('aria-singer', 'true'); // unknown
147 |
148 | var config = {
149 | ruleName: RULE_NAME,
150 | expected: axs.constants.AuditResult.FAIL,
151 | elements: [widget]
152 | };
153 | assert.runRule(config, 'This rule should detect unknown "aria-" attributes amongst known ones');
154 | });
155 |
156 | })();
157 |
--------------------------------------------------------------------------------
/test/js/audit-rule-test.js:
--------------------------------------------------------------------------------
1 | // Copyright 2014 Google Inc.
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | (function(){
15 | module("collectMatchingElements", {
16 | setup: function() {
17 | // Recreate the dummy objects before each test to ensure there are no carried over results
18 | dummyRule = new axs.AuditRule({
19 | name: 'badFishingHole',
20 | heading: 'Tests if this is a good place to go fishing',
21 | url: 'http://www.example.com/troutfishing',
22 | severity: axs.constants.Severity.SEVERE,
23 | relevantElementMatcher: function(element) {
24 | var tagName = element.tagName;
25 | if (!tagName)
26 | return false;
27 | return (tagName.toLowerCase() === 'div' && element.classList.contains('test'));
28 | },
29 | test: function(element) {
30 | return false;
31 | },
32 | code: 'AX_TROUT_01'
33 | });
34 | dummyConfig = new axs.AuditConfiguration();
35 | }
36 | });
37 |
38 | var dummyRule, dummyConfig, DIV_COUNT = 10;
39 |
40 | function buildTestDom() {
41 | var result = document.createDocumentFragment();
42 | result = result.appendChild(document.createElement("div"));
43 | for (var i = 0; i < DIV_COUNT; i++) {
44 | var element = document.createElement("div");
45 | element.className = "test";
46 | element.id = "test-" + i;
47 | result.appendChild(element);
48 | }
49 | return result;
50 | }
51 |
52 | test("Simple DOM", function () {
53 | var container = document.getElementById('qunit-fixture');
54 | container.appendChild(buildTestDom());
55 | dummyConfig.scope = container;
56 | axs.Audit.collectMatchingElements(dummyConfig, [dummyRule]);
57 | equal(dummyRule.relevantElements.length, DIV_COUNT);
58 | });
59 |
60 | test("Simple DOM with an ignored selector", function () {
61 | var container = document.getElementById('qunit-fixture');
62 | container.appendChild(buildTestDom());
63 | var fooElement = document.createElement('div');
64 | fooElement.className = 'foo';
65 | container.appendChild(fooElement);
66 | var fooTest = document.createElement('div');
67 | fooTest.className = 'test';
68 | fooElement.appendChild(fooTest);
69 |
70 | dummyConfig.scope = container;
71 | dummyConfig.getIgnoreSelectors = function() {
72 | return ['.foo'];
73 | };
74 | axs.Audit.collectMatchingElements(dummyConfig, [dummyRule]);
75 | equal(dummyRule.relevantElements.length, DIV_COUNT);
76 | });
77 |
78 | test("With shadow DOM with no content insertion point", function () {
79 | var container = document.getElementById('qunit-fixture');
80 | container.appendChild(buildTestDom());
81 | var wrapper = container.firstElementChild;
82 | if (wrapper.createShadowRoot) {
83 | wrapper.createShadowRoot();
84 | dummyConfig.scope = container;
85 | axs.Audit.collectMatchingElements(dummyConfig, [dummyRule]);
86 | equal(dummyRule.relevantElements.length, 0);
87 | } else {
88 | console.warn("Test platform does not support shadow DOM");
89 | ok(true);
90 | }
91 | });
92 |
93 | test("With shadow DOM with content element", function () {
94 | var container = document.getElementById('qunit-fixture');
95 | container.appendChild(buildTestDom());
96 | var wrapper = container.firstElementChild;
97 | if (wrapper.createShadowRoot) {
98 | var root = wrapper.createShadowRoot();
99 | var content = document.createElement('content');
100 | root.appendChild(content);
101 | dummyConfig.scope = container;
102 | axs.Audit.collectMatchingElements(dummyConfig, [dummyRule]);
103 | // picks up content
104 | equal(dummyRule.relevantElements.length, DIV_COUNT);
105 | } else {
106 | console.warn("Test platform does not support shadow DOM");
107 | ok(true);
108 | }
109 | });
110 |
111 | test("Nodes within shadow DOM", function () {
112 | var container = document.getElementById('qunit-fixture');
113 | var wrapper = container.appendChild(document.createElement("div"));
114 | if (wrapper.createShadowRoot) {
115 | var root = wrapper.createShadowRoot();
116 | root.appendChild(buildTestDom());
117 | dummyConfig.scope = container;
118 | axs.Audit.collectMatchingElements(dummyConfig, [dummyRule]);
119 | // Nodes in shadows are found
120 | equal(dummyRule.relevantElements.length, DIV_COUNT);
121 | } else {
122 | console.warn("Test platform does not support shadow DOM");
123 | ok(true);
124 | }
125 | });
126 |
127 | test("Nodes within DOM and shadow DOM - no content distribution point", function () {
128 | var container = document.getElementById('qunit-fixture');
129 | var wrapper = container.appendChild(document.createElement("div"));
130 | if (wrapper.createShadowRoot) {
131 | var root = wrapper.createShadowRoot();
132 | var rootContent = document.createElement('div');
133 | rootContent.className = 'test';
134 | root.appendChild(rootContent);
135 | wrapper.appendChild(buildTestDom());
136 | dummyConfig.scope = container;
137 | axs.Audit.collectMatchingElements(dummyConfig, [dummyRule]);
138 | // Nodes in light dom are not distributed
139 | equal(dummyRule.relevantElements.length, 1);
140 | } else {
141 | console.warn("Test platform does not support shadow DOM");
142 | ok(true);
143 | }
144 | });
145 |
146 | test("Nodes within DOM and shadow DOM with content element", function () {
147 | var container = document.getElementById('qunit-fixture');
148 | var wrapper = container.appendChild(document.createElement("div"));
149 | wrapper.appendChild(buildTestDom());
150 | if (wrapper.createShadowRoot) {
151 | var root = wrapper.createShadowRoot();
152 | var rootContent = document.createElement('div');
153 | rootContent.className = 'test';
154 | root.appendChild(rootContent);
155 | var content = document.createElement('content');
156 | root.appendChild(content);
157 | dummyConfig.scope = container;
158 | axs.Audit.collectMatchingElements(dummyConfig, [dummyRule]);
159 | // Nodes in light dom are distributed into content element.
160 | equal(dummyRule.relevantElements.length, (DIV_COUNT + 1));
161 | } else {
162 | console.warn("Test platform does not support shadow DOM");
163 | ok(true);
164 | }
165 | });
166 | })();
167 |
--------------------------------------------------------------------------------