├── .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 |

Test Accessibility Extension

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 | ' \n' + 109 | ' \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 |

    Test Accessibility Extension

    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 | --------------------------------------------------------------------------------