diff --git a/lib/commons/aria/implicit-role.js b/lib/commons/aria/implicit-role.js index c1bf91680b..eab0cc29b3 100644 --- a/lib/commons/aria/implicit-role.js +++ b/lib/commons/aria/implicit-role.js @@ -1,5 +1,5 @@ -import allowedAttr from './allowed-attr'; import lookupTable from './lookup-table'; +import { getNodeFromTree } from '../../core/utils'; /** * Get the implicit role for a given node @@ -9,86 +9,37 @@ import lookupTable from './lookup-table'; * @param {HTMLElement} node The node to test * @return {Mixed} Either the role or `null` if there is none */ -// TODO: axe 4.0 - rename to `getImplicitRole` so we can use -// a variable named `implicitRole` when assigning the output of -// the function function implicitRole(node) { - 'use strict'; - - /* - * Filter function to reduce a list of roles to a valid list of roles for a nodetype - */ - const isValidImplicitRole = function(set, role) { - const validForNodeType = function(implicitNodeTypeSelector) { - // TODO: es-module-utils.matchesSelector - return axe.utils.matchesSelector(node, implicitNodeTypeSelector); - }; - - if (role.implicit && role.implicit.some(validForNodeType)) { - set.push(role.name); - } - - return set; - }; - - /* - * Score a set of roles and aria-attributes by its optimal score - * E.g. [{score: 2, name: button}, {score: 1, name: main}] - */ - const sortRolesByOptimalAriaContext = function(roles, ariaAttributes) { - const getScore = function(role) { - const allowedAriaAttributes = allowedAttr(role); - return allowedAriaAttributes.reduce(function(score, attribute) { - return score + (ariaAttributes.indexOf(attribute) > -1 ? 1 : 0); - }, 0); - }; - - const scored = roles.map(function(role) { - return { score: getScore(role), name: role }; - }); - - const sorted = scored.sort(function(scoredRoleA, scoredRoleB) { - return scoredRoleB.score - scoredRoleA.score; - }); - - return sorted.map(function(sortedRole) { - return sortedRole.name; - }); - }; + const vNode = getNodeFromTree(node); + + // this error is only thrown if the virtual tree is not a + // complete tree, which only happens in linting and if a + // user used `getFlattenedTree` manually on a subset of the + // DOM tree + if (!vNode) { + throw new ReferenceError( + 'Cannot get implicit role of a node outside the current scope.' + ); + } - /* - * Create a list of { name / implicit } role mappings to filter on - */ - const roles = Object.keys(lookupTable.role).map(function(role) { - const lookup = lookupTable.role[role]; - return { name: role, implicit: lookup && lookup.implicit }; - }); + // until we have proper implicit role lookups for svgs we will + // avoid giving them one + if (node.namespaceURI === 'http://www.w3.org/2000/svg') { + return null; + } - /* Build a list of valid implicit roles for this node */ - const availableImplicitRoles = roles.reduce(isValidImplicitRole, []); + const nodeName = vNode.props.nodeName; + const role = lookupTable.implicitHtmlRole[nodeName]; - if (!availableImplicitRoles.length) { + if (!role) { return null; } - // TODO: es-module-utils.getNodeAttributes - const nodeAttributes = axe.utils.getNodeAttributes(node); - const ariaAttributes = []; - - /* Get all aria-attributes defined for this node */ - /* Should be a helper function somewhere */ - for (let i = 0, j = nodeAttributes.length; i < j; i++) { - const attr = nodeAttributes[i]; - if (attr.name.match(/^aria-/)) { - ariaAttributes.push(attr.name); - } + if (typeof role === 'function') { + return role(vNode); } - /* Sort roles by highest score, return the first */ - return sortRolesByOptimalAriaContext( - availableImplicitRoles, - ariaAttributes - ).shift(); + return role; } export default implicitRole; diff --git a/lib/commons/aria/lookup-table.js b/lib/commons/aria/lookup-table.js index 9cc621c08b..c73997b5a7 100644 --- a/lib/commons/aria/lookup-table.js +++ b/lib/commons/aria/lookup-table.js @@ -1,3 +1,11 @@ +import arialabelledbyText from './arialabelledby-text'; +import arialabelText from './arialabel-text'; +import idrefs from '../dom/idrefs'; +import isColumnHeader from '../table/is-column-header'; +import isRowHeader from '../table/is-row-header'; +import sanitize from '../text/sanitize'; +import { closest } from '../../core/utils'; + const isNull = value => value === null; const isNotNull = value => value !== null; @@ -1012,6 +1020,13 @@ lookupTable.role = { }, figure: { type: 'structure', + attributes: { + allowed: ['aria-expanded', 'aria-errormessage'] + }, + owned: null, + nameFrom: ['author', 'contents'], + context: null, + implicit: ['figure'], unsupported: false }, form: { @@ -1131,7 +1146,7 @@ lookupTable.role = { owned: null, nameFrom: ['author', 'contents'], context: null, - implicit: ['a[href]'], + implicit: ['a[href]', 'area[href]'], unsupported: false, allowedElements: [ 'button', @@ -2079,6 +2094,151 @@ lookupTable.role = { } }; +const sectioningElementSelector = + 'article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]'; + +// sectioning elements only have an accessible name if the +// aria-label, aria-labelledby, or title attribute has valid +// content. +// can't go through the normal accessible name computation +// as it leads into an infinite loop of asking for the role +// of the element while the implicit role needs the name. +// Source: https://www.w3.org/TR/html-aam-1.0/#section-and-grouping-element-accessible-name-computation +// +// form elements also follow this same pattern although not +// specifically called out in the spec like section elements +// (per Scott O'Hara) +// Source: https://web-a11y.slack.com/archives/C042TSFGN/p1590607895241100?thread_ts=1590602189.217800&cid=C042TSFGN +function hasAccessibleName(vNode) { + return !!( + // testing for when browsers give a
a region role: + // chrome - always a region role + // firefox - if non-empty aria-labelledby, aria-label, or title + // safari - if non-empty aria-lablledby or aria-label + // + // we will go with safaris implantation as it is the least common + // denominator + ( + (vNode.hasAttr('aria-labelledby') && + sanitize(arialabelledbyText(vNode))) || + (vNode.hasAttr('aria-label') && sanitize(arialabelText(vNode))) + ) + ); +} + +// Source: https://www.w3.org/TR/html-aam-1.0/#element-mapping-table +// Source: https://www.w3.org/TR/html-aria/ +lookupTable.implicitHtmlRole = { + a: vNode => { + return vNode.hasAttr('href') ? 'link' : null; + }, + area: vNode => { + return vNode.hasAttr('href') ? 'link' : null; + }, + article: 'article', + aside: 'complementary', + body: 'document', + button: 'button', + datalist: 'listbox', + dd: 'definition', + dfn: 'term', + details: 'group', + dialog: 'dialog', + dt: 'term', + fieldset: 'group', + figure: 'figure', + footer: vNode => { + const sectioningElement = closest(vNode, sectioningElementSelector); + + return !sectioningElement ? 'contentinfo' : null; + }, + form: vNode => { + return hasAccessibleName(vNode) ? 'form' : null; + }, + h1: 'heading', + h2: 'heading', + h3: 'heading', + h4: 'heading', + h5: 'heading', + h6: 'heading', + header: vNode => { + const sectioningElement = closest(vNode, sectioningElementSelector); + + return !sectioningElement ? 'banner' : null; + }, + hr: 'separator', + img: vNode => { + return vNode.hasAttr('alt') && !vNode.attr('alt') ? null : 'img'; + }, + input: vNode => { + // Source: https://www.w3.org/TR/html52/sec-forms.html#suggestions-source-element + const listElement = idrefs(vNode.actualNode, 'list').filter( + node => !!node + )[0]; + const suggestionsSourceElement = + listElement && listElement.nodeName.toLowerCase() === 'datalist'; + + switch ((vNode.attr('type') || '').toLowerCase()) { + case 'button': + case 'image': + case 'reset': + case 'submit': + return 'button'; + case 'checkbox': + return 'checkbox'; + case 'email': + case 'tel': + case 'text': + case 'url': + case '': // text is the default value + return !suggestionsSourceElement ? 'textbox' : 'combobox'; + case 'number': + return 'spinbutton'; + case 'radio': + return 'radio'; + case 'range': + return 'slider'; + case 'search': + return !suggestionsSourceElement ? 'searchbox' : 'combobox'; + } + }, + li: 'listitem', + main: 'main', + math: 'math', + menu: 'list', + nav: 'navigation', + ol: 'list', + optgroup: 'group', + option: 'option', + output: 'status', + progress: 'progressbar', + section: vNode => { + return hasAccessibleName(vNode) ? 'region' : null; + }, + select: vNode => { + return vNode.hasAttr('multiple') || parseInt(vNode.attr('size')) > 1 + ? 'listbox' + : 'combobox'; + }, + summary: 'button', + table: 'table', + tbody: 'rowgroup', + td: 'cell', + textarea: 'textbox', + tfoot: 'rowgroup', + th: vNode => { + if (isColumnHeader(vNode.actualNode)) { + return 'columnheader'; + } + if (isRowHeader(vNode.actualNode)) { + return 'rowheader'; + } + }, + thead: 'rowgroup', + tr: 'row', + ul: 'list' +}; + // Source: https://www.w3.org/TR/html-aria/ lookupTable.elementsAllowedNoRole = [ { diff --git a/test/checks/aria/allowed-attr.js b/test/checks/aria/allowed-attr.js index 1af49c659c..fe8719c2ec 100644 --- a/test/checks/aria/allowed-attr.js +++ b/test/checks/aria/allowed-attr.js @@ -2,6 +2,7 @@ describe('aria-allowed-attr', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var flatTreeSetup = axe.testUtils.flatTreeSetup; var checkContext = axe.testUtils.MockCheckContext(); afterEach(function() { @@ -16,6 +17,7 @@ describe('aria-allowed-attr', function() { node.tabIndex = 1; node.setAttribute('aria-selected', 'true'); fixture.appendChild(node); + flatTreeSetup(fixture); assert.isFalse( axe.testUtils @@ -32,6 +34,7 @@ describe('aria-allowed-attr', function() { node.tabIndex = 1; node.setAttribute('aria-checked', 'true'); fixture.appendChild(node); + flatTreeSetup(fixture); assert.isTrue( axe.testUtils @@ -47,6 +50,7 @@ describe('aria-allowed-attr', function() { node.tabIndex = 1; node.setAttribute('aria-selected', 'true'); fixture.appendChild(node); + flatTreeSetup(fixture); assert.isFalse( axe.testUtils @@ -63,6 +67,7 @@ describe('aria-allowed-attr', function() { node.setAttribute('aria-selected', 'true'); node.setAttribute('aria-checked', 'true'); fixture.appendChild(node); + flatTreeSetup(fixture); assert.isTrue( axe.testUtils @@ -79,6 +84,7 @@ describe('aria-allowed-attr', function() { node.setAttribute('aria-cats', 'true'); node.setAttribute('role', 'dialog'); fixture.appendChild(node); + flatTreeSetup(fixture); assert.isTrue( axe.testUtils @@ -96,6 +102,7 @@ describe('aria-allowed-attr', function() { node.setAttribute('aria-required', 'true'); node.setAttribute('aria-checked', 'true'); fixture.appendChild(node); + flatTreeSetup(fixture); assert.isTrue( axe.testUtils @@ -119,6 +126,7 @@ describe('aria-allowed-attr', function() { fixture.innerHTML = '
'; var target = fixture.children[0]; + flatTreeSetup(fixture); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-attr') @@ -155,6 +163,7 @@ describe('aria-allowed-attr', function() { mccheddarton: ['aria-snuggles'], bagley: ['aria-snuggles2'] }; + flatTreeSetup(fixture); assert.isTrue( axe.testUtils .getCheckEvaluate('aria-allowed-attr') diff --git a/test/checks/aria/aria-roledescription.js b/test/checks/aria/aria-roledescription.js index 96194594d3..7cbd7745ad 100644 --- a/test/checks/aria/aria-roledescription.js +++ b/test/checks/aria/aria-roledescription.js @@ -2,6 +2,7 @@ describe('aria-roledescription', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var flatTreeSetup = axe.testUtils.flatTreeSetup; var checkContext = axe.testUtils.MockCheckContext(); afterEach(function() { @@ -12,6 +13,7 @@ describe('aria-roledescription', function() { it('returns true for elements with an implicit supported role', function() { fixture.innerHTML = ''; + flatTreeSetup(fixture); var actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, fixture.firstChild, { @@ -24,6 +26,7 @@ describe('aria-roledescription', function() { it('returns true for elements with an explicit supported role', function() { fixture.innerHTML = '
Click
'; + flatTreeSetup(fixture); var actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, fixture.firstChild, { @@ -36,6 +39,7 @@ describe('aria-roledescription', function() { it('returns undefined for elements with an unsupported role', function() { fixture.innerHTML = '
The main element
'; + flatTreeSetup(fixture); var actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, fixture.firstChild); @@ -46,6 +50,7 @@ describe('aria-roledescription', function() { it('returns false for elements without role', function() { fixture.innerHTML = '
The main element
'; + flatTreeSetup(fixture); var actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, fixture.firstChild); @@ -56,6 +61,7 @@ describe('aria-roledescription', function() { it('returns false for elements with role=presentation', function() { fixture.innerHTML = '
The main element
'; + flatTreeSetup(fixture); var actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, fixture.firstChild); @@ -66,6 +72,7 @@ describe('aria-roledescription', function() { it('returns false for elements with role=none', function() { fixture.innerHTML = '
The main element
'; + flatTreeSetup(fixture); var actual = axe.testUtils .getCheckEvaluate('aria-roledescription') .call(checkContext, fixture.firstChild); diff --git a/test/checks/aria/unsupportedrole.js b/test/checks/aria/unsupportedrole.js index 272ecd5efc..cef773b380 100644 --- a/test/checks/aria/unsupportedrole.js +++ b/test/checks/aria/unsupportedrole.js @@ -2,6 +2,7 @@ describe('unsupportedrole', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var flatTreeSetup = axe.testUtils.flatTreeSetup; afterEach(function() { fixture.innerHTML = ''; @@ -14,22 +15,26 @@ describe('unsupportedrole', function() { }; fixture.innerHTML = '
Contents
'; var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); assert.isTrue(checks.unsupportedrole.evaluate(node)); }); it('should return false if applied to a supported role', function() { fixture.innerHTML = ''; var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); assert.isFalse(checks.unsupportedrole.evaluate(node)); fixture.innerHTML = ''; var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); assert.isFalse(checks.unsupportedrole.evaluate(node)); }); it('should return false if applied to an invalid role', function() { fixture.innerHTML = ''; var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); assert.isFalse(checks.unsupportedrole.evaluate(node)); }); }); diff --git a/test/checks/keyboard/landmark-is-top-level.js b/test/checks/keyboard/landmark-is-top-level.js index e7cf35f017..5372f1905b 100644 --- a/test/checks/keyboard/landmark-is-top-level.js +++ b/test/checks/keyboard/landmark-is-top-level.js @@ -15,6 +15,8 @@ describe('landmark-is-top-level', function() { var params = checkSetup( '
' ); + // landmark-is-top-level requires a complete tree to work properly + axe.utils.getFlattenedTree(document.documentElement); assert.isFalse(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { role: 'main' }); }); @@ -23,6 +25,7 @@ describe('landmark-is-top-level', function() { var params = checkSetup( '
' ); + axe.utils.getFlattenedTree(document.documentElement); assert.isFalse(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { role: 'complementary' }); }); @@ -31,6 +34,7 @@ describe('landmark-is-top-level', function() { var params = checkSetup( '
' ); + axe.utils.getFlattenedTree(document.documentElement); assert.isFalse(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { role: 'main' }); }); @@ -39,6 +43,7 @@ describe('landmark-is-top-level', function() { var params = checkSetup( '
' ); + axe.utils.getFlattenedTree(document.documentElement); assert.isTrue(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { role: 'contentinfo' }); }); @@ -47,6 +52,7 @@ describe('landmark-is-top-level', function() { var params = checkSetup( '
' ); + axe.utils.getFlattenedTree(document.documentElement); assert.isTrue(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { role: 'main' }); }); @@ -55,6 +61,7 @@ describe('landmark-is-top-level', function() { var params = checkSetup( '
' ); + axe.utils.getFlattenedTree(document.documentElement); assert.isTrue(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { role: 'banner' }); }); @@ -66,6 +73,7 @@ describe('landmark-is-top-level', function() { '
', '
Main content
' ); + axe.utils.getFlattenedTree(document.documentElement); assert.isTrue(check.evaluate.apply(checkContext, params)); assert.deepEqual(checkContext._data, { role: 'main' }); } diff --git a/test/commons/aria/get-role.js b/test/commons/aria/get-role.js index 83a1180801..daeb97994a 100644 --- a/test/commons/aria/get-role.js +++ b/test/commons/aria/get-role.js @@ -2,6 +2,7 @@ describe('aria.getRole', function() { 'use strict'; var aria = axe.commons.aria; var roleDefinitions = aria.lookupTable.role; + var flatTreeSetup = axe.testUtils.flatTreeSetup; var orig; beforeEach(function() { @@ -15,74 +16,87 @@ describe('aria.getRole', function() { it('returns valid roles', function() { var node = document.createElement('div'); node.setAttribute('role', 'button'); + flatTreeSetup(node); assert.equal(aria.getRole(node), 'button'); }); it('handles case sensitivity', function() { var node = document.createElement('div'); node.setAttribute('role', 'BUTTON'); + flatTreeSetup(node); assert.equal(aria.getRole(node), 'button'); }); it('handles whitespacing', function() { var node = document.createElement('div'); node.setAttribute('role', ' button '); + flatTreeSetup(node); assert.equal(aria.getRole(node), 'button'); }); it('returns null when there is no role', function() { var node = document.createElement('div'); + flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); it('returns the explit role if it is valid and non-abstract', function() { var node = document.createElement('li'); node.setAttribute('role', 'menuitem'); + flatTreeSetup(node); assert.equal(aria.getRole(node), 'menuitem'); }); it('returns the implicit role if the explicit is invalid', function() { var node = document.createElement('li'); node.setAttribute('role', 'foobar'); + flatTreeSetup(node); assert.equal(aria.getRole(node), 'listitem'); }); it('ignores fallback roles by default', function() { var node = document.createElement('div'); node.setAttribute('role', 'spinbutton button'); + flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); it('accepts virtualNode objects', function() { var node = document.createElement('div'); node.setAttribute('role', 'button'); + flatTreeSetup(node); assert.equal(aria.getRole({ actualNode: node }), 'button'); }); it('returns null if the node is not an element', function() { var node = document.createTextNode('foo bar baz'); + flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); describe('noImplicit', function() { it('returns the implicit role by default', function() { var node = document.createElement('li'); + flatTreeSetup(node); assert.equal(aria.getRole(node), 'listitem'); }); it('returns null rather than the implicit role with `noImplicit: true`', function() { var node = document.createElement('li'); + flatTreeSetup(node); assert.isNull(aria.getRole(node, { noImplicit: true })); }); it('still returns the explicit role', function() { var node = document.createElement('li'); node.setAttribute('role', 'button'); + flatTreeSetup(node); assert.equal(aria.getRole(node, { noImplicit: true }), 'button'); }); it('returns the implicit role with `noImplicit: false`', function() { var node = document.createElement('li'); + flatTreeSetup(node); assert.equal(aria.getRole(node, { noImplicit: false }), 'listitem'); }); }); @@ -91,6 +105,7 @@ describe('aria.getRole', function() { it('ignores abstract roles by default', function() { var node = document.createElement('li'); node.setAttribute('role', 'section'); + flatTreeSetup(node); assert.equal(roleDefinitions.section.type, 'abstract'); assert.equal(aria.getRole(node), 'listitem'); }); @@ -98,6 +113,7 @@ describe('aria.getRole', function() { it('returns abstract roles with `abstracts: true`', function() { var node = document.createElement('li'); node.setAttribute('role', 'section'); + flatTreeSetup(node); assert.equal(roleDefinitions.section.type, 'abstract'); assert.equal(aria.getRole(node, { abstracts: true }), 'section'); }); @@ -105,6 +121,7 @@ describe('aria.getRole', function() { it('does not returns abstract roles with `abstracts: false`', function() { var node = document.createElement('li'); node.setAttribute('role', 'section'); + flatTreeSetup(node); assert.equal(roleDefinitions.section.type, 'abstract'); assert.equal(aria.getRole(node, { abstracts: false }), 'listitem'); }); @@ -114,18 +131,21 @@ describe('aria.getRole', function() { it('ignores DPUB roles by default', function() { var node = document.createElement('section'); node.setAttribute('role', 'doc-chapter'); + flatTreeSetup(node); assert.isNull(aria.getRole(node)); }); it('returns DPUB roles with `dpub: true`', function() { var node = document.createElement('section'); node.setAttribute('role', 'doc-chapter'); + flatTreeSetup(node); assert.equal(aria.getRole(node, { dpub: true }), 'doc-chapter'); }); it('does not returns DPUB roles with `dpub: false`', function() { var node = document.createElement('section'); node.setAttribute('role', 'doc-chapter'); + flatTreeSetup(node); assert.isNull(aria.getRole(node, { dpub: false })); }); }); @@ -134,36 +154,42 @@ describe('aria.getRole', function() { it('returns the first valid item in the list', function() { var node = document.createElement('div'); node.setAttribute('role', 'link button'); + flatTreeSetup(node); assert.equal(aria.getRole(node, { fallback: true }), 'link'); }); it('skips over invalid roles', function() { var node = document.createElement('div'); node.setAttribute('role', 'foobar button'); + flatTreeSetup(node); assert.equal(aria.getRole(node, { fallback: true }), 'button'); }); it('returns the null if all roles are invalid and there is no implicit role', function() { var node = document.createElement('div'); node.setAttribute('role', 'foo bar baz'); + flatTreeSetup(node); assert.isNull(aria.getRole(node, { fallback: true })); }); it('respects the defaults', function() { var node = document.createElement('li'); node.setAttribute('role', 'doc-chapter section'); + flatTreeSetup(node); assert.equal(aria.getRole(node, { fallback: true }), 'listitem'); }); it('respect the `noImplicit` option', function() { var node = document.createElement('li'); node.setAttribute('role', 'doc-chapter section'); + flatTreeSetup(node); assert.isNull(aria.getRole(node, { fallback: true, noImplicit: true })); }); it('respect the `abstracts` option', function() { var node = document.createElement('li'); node.setAttribute('role', 'doc-chapter section'); + flatTreeSetup(node); assert.equal( aria.getRole(node, { fallback: true, abstracts: true }), 'section' @@ -173,6 +199,7 @@ describe('aria.getRole', function() { it('respect the `dpub` option', function() { var node = document.createElement('li'); node.setAttribute('role', 'doc-chapter section'); + flatTreeSetup(node); assert.equal( aria.getRole(node, { fallback: true, dpub: true }), 'doc-chapter' diff --git a/test/commons/aria/implicit-role.js b/test/commons/aria/implicit-role.js new file mode 100644 index 0000000000..41ef00ff2a --- /dev/null +++ b/test/commons/aria/implicit-role.js @@ -0,0 +1,389 @@ +describe('aria.implicitRole', function() { + 'use strict'; + var implicitRole = axe.commons.aria.implicitRole; + var flatTreeSetup = axe.testUtils.flatTreeSetup; + var fixture = document.querySelector('#fixture'); + + afterEach(function() { + fixture.innerHTML = ''; + }); + + // test string role (don't need to test all of them just that + // one works) + it('should return button for button', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'button'); + }); + + it('should error if element is not in the tree', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + assert.throws(function() { + implicitRole(node); + }); + }); + + it('should return null if there is no implicit role', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return link for "a[href]"', function() { + fixture.innerHTML = 'link'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'link'); + }); + + it('should return null for "a:not([href])"', function() { + fixture.innerHTML = 'link'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return link for "area[href]"', function() { + fixture.innerHTML = 'link'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'link'); + }); + + it('should return null for "area:not([href])"', function() { + fixture.innerHTML = 'link'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return contentinfo for "body footer"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'contentinfo'); + }); + + it('should return null for footer with sectioning parent', function() { + var nodes = ['article', 'aside', 'main', 'nav', 'section']; + var roles = ['article', 'complementary', 'main', 'navigation', 'region']; + + for (var i = 0; i < nodes.length; i++) { + fixture.innerHTML = + '<' + nodes[i] + '>'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node), nodes[i] + ' not null'); + } + + for (var i = 0; i < roles.length; i++) { + fixture.innerHTML = + '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node), '[' + roles[i] + '] not null'); + } + }); + + it('should return form for form with accessible name aria-label', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'form'); + }); + + it('should return form for form with accessible name aria-labelledby', function() { + fixture.innerHTML = + '
foo
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'form'); + }); + + it('should return null for form with accessible name title', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return null for form without accessible name', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return banner for "body header"', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'banner'); + }); + + it('should return null for header with sectioning parent', function() { + var nodes = ['article', 'aside', 'main', 'nav', 'section']; + var roles = ['article', 'complementary', 'main', 'navigation', 'region']; + + for (var i = 0; i < nodes.length; i++) { + fixture.innerHTML = + '<' + nodes[i] + '>
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node), nodes[i] + ' not null'); + } + + for (var i = 0; i < roles.length; i++) { + fixture.innerHTML = + '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node), '[' + roles[i] + '] not null'); + } + }); + + it('should return img for "img[alt]"', function() { + fixture.innerHTML = 'value'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'img'); + }); + + it('should return img for "img:not([alt])"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'img'); + }); + + it('should return null for "img" with empty alt', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return button for "input[type=button]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'button'); + }); + + it('should return button for "input[type=image]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'button'); + }); + + it('should return button for "input[type=reset]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'button'); + }); + + it('should return button for "input[type=submit]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'button'); + }); + + it('should return checkbox for "input[type=checkbox]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'checkbox'); + }); + + it('should return textbox for "input[type=email]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'textbox'); + }); + + it('should return textbox for "input[type=tel]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'textbox'); + }); + + it('should return textbox for "input[type=text]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'textbox'); + }); + + it('should return textbox for "input[type=url]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'textbox'); + }); + + it('should return textbox for "input:not([type])"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'textbox'); + }); + + it('should return combobox for "input[list]" that points to a datalist', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'combobox'); + }); + + it('should return textbox for "input[list]" that does not point to a datalist', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'textbox'); + }); + + it('should return spinbutton for "input[type=number]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'spinbutton'); + }); + + it('should return radio for "input[type=radio]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'radio'); + }); + + it('should return slider for "input[type=range]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'slider'); + }); + + it('should return searchbox for "input[type=search]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'searchbox'); + }); + + it('should return combobox for "input[type=search][list]"', function() { + fixture.innerHTML = + ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'combobox'); + }); + + it('should return region for "section" with accessible name aria-label', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'region'); + }); + + it('should return region for section with accessible name aria-labelledby', function() { + fixture.innerHTML = + '
foo
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'region'); + }); + + it('should return null for section with accessible name title', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return null for "section" without accessible name', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return null for "section" with empty aria-label', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return null for "section" with empty aria-labelledby', function() { + fixture.innerHTML = + '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return null for "section" with empty title', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.isNull(implicitRole(node)); + }); + + it('should return listbox for "select[multiple]"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'listbox'); + }); + + it('should return listbox for "select[size]" > 1', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'listbox'); + }); + + it('should return combobox for "select[size]" <= 1', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'combobox'); + }); + + it('should return combobox for "select"', function() { + fixture.innerHTML = ''; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'combobox'); + }); + + it('should return cell for "td"', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'cell'); + }); + + it('should return rowheader for "th[scope=row]"', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'rowheader'); + }); + + it('should return columnheader for "th[scope=col]"', function() { + fixture.innerHTML = '
'; + var node = fixture.querySelector('#target'); + flatTreeSetup(fixture); + assert.equal(implicitRole(node), 'columnheader'); + }); +}); diff --git a/test/commons/aria/named-from-contents.js b/test/commons/aria/named-from-contents.js index fbe921b075..e50b947fcf 100644 --- a/test/commons/aria/named-from-contents.js +++ b/test/commons/aria/named-from-contents.js @@ -2,6 +2,7 @@ describe('aria.namedFromContents', function() { var aria = axe.commons.aria; var namedFromContents = aria.namedFromContents; var fixture = document.querySelector('#fixture'); + var flatTreeSetup = axe.testUtils.flatTreeSetup; beforeEach(function() { aria.foo = { nameFrom: ['author', 'contents'] }; @@ -16,16 +17,19 @@ describe('aria.namedFromContents', function() { it('returns true when the element has an explicit role named from content', function() { fixture.innerHTML = '
'; + flatTreeSetup(fixture); assert.isTrue(namedFromContents(fixture.firstChild)); }); it('works on virtual nodes', function() { fixture.innerHTML = '
'; + flatTreeSetup(fixture); assert.isTrue(namedFromContents({ actualNode: fixture.firstChild })); }); it('returns true when the element has an implicit role named from content', function() { fixture.innerHTML = '

foo

'; + flatTreeSetup(fixture); // Varify h1 is named from contents: assert.equal(aria.getRole(fixture.firstChild), 'heading'); assert.include(aria.lookupTable.role.heading.nameFrom, 'contents'); @@ -35,6 +39,7 @@ describe('aria.namedFromContents', function() { it('returns false when the element has a role not named from content', function() { fixture.innerHTML = '
'; + flatTreeSetup(fixture); // Varify main is not named from contents: assert.equal(aria.getRole(fixture.firstChild), 'main'); assert.notInclude(aria.lookupTable.role.main.nameFrom, 'contents'); @@ -44,34 +49,40 @@ describe('aria.namedFromContents', function() { it('returns false node is not a DOM element', function() { fixture.innerHTML = 'text node'; + flatTreeSetup(fixture); assert.isFalse(namedFromContents(fixture.firstChild)); }); describe('{ strict: false }', function() { it('returns true when the element has no role named from content', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isNull(aria.getRole(fixture.firstChild)); assert.isTrue(namedFromContents(fixture.firstChild, { strict: false })); }); it('is default for aria.namedFromContents', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isNull(aria.getRole(fixture.firstChild)); assert.isTrue(namedFromContents(fixture.firstChild)); }); it('returns true when the element has role=presentation', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isTrue(namedFromContents(fixture.firstChild, { strict: false })); }); it('returns true when the element has role=none', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isTrue(namedFromContents(fixture.firstChild, { strict: false })); }); it('returns true when the implicit role is null', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isTrue(namedFromContents(fixture.firstChild, { strict: false })); }); }); @@ -79,17 +90,20 @@ describe('aria.namedFromContents', function() { describe('{ strict: true }', function() { it('returns false when the element has no role named from content', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isNull(aria.getRole(fixture.firstChild)); assert.isFalse(namedFromContents(fixture.firstChild, { strict: true })); }); it('returns false when the element has role=presentation', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isFalse(namedFromContents(fixture.firstChild, { strict: true })); }); it('returns false when the element has role=none', function() { fixture.innerHTML = '
foo
'; + flatTreeSetup(fixture); assert.isFalse(namedFromContents(fixture.firstChild, { strict: true })); }); }); diff --git a/test/commons/aria/roles.js b/test/commons/aria/roles.js index 65452695c7..33444acafa 100644 --- a/test/commons/aria/roles.js +++ b/test/commons/aria/roles.js @@ -172,82 +172,3 @@ describe('aria.implicitNodes', function() { assert.isNull(result); }); }); - -describe('aria.implicitRole', function() { - 'use strict'; - - var fixture = document.getElementById('fixture'); - var orig; - beforeEach(function() { - orig = axe.commons.aria.lookupTable.role; - }); - - afterEach(function() { - fixture.innerHTML = ''; - axe.commons.aria.lookupTable.role = orig; - }); - - it('should find the first optimal matching role', function() { - var node = document.createElement('div'); - node.setAttribute('aria-required', 'true'); - - node.id = 'cats'; - fixture.appendChild(node); - - axe.commons.aria.lookupTable.role = { - cats: { - implicit: ['div[id="cats"]'] - }, - dogs: { - implicit: ['div[id="cats"]'] - }, - dinosaurs: { - attributes: { - allowed: ['aria-required'] - }, - implicit: ['div[id="cats"]'] - } - }; - - assert.equal(axe.commons.aria.implicitRole(node), 'dinosaurs'); - }); - - it('should find the first optimal matching role when multiple optimal matches are available', function() { - var node = document.createElement('div'); - node.setAttribute('aria-required', 'true'); - - node.id = 'cats'; - fixture.appendChild(node); - - axe.commons.aria.lookupTable.role = { - cats: { - implicit: ['div[id="cats"]'] - }, - dogs: { - attributes: { - allowed: ['aria-required'] - }, - implicit: ['div[id="cats"]'] - }, - dinosaurs: { - attributes: { - allowed: ['aria-required'] - }, - implicit: ['div[id="cats"]'] - } - }; - - assert.equal(axe.commons.aria.implicitRole(node), 'dogs'); - }); - - it('should return null if there is no matching implicit role', function() { - var node = document.createElement('div'); - node.id = 'cats'; - fixture.appendChild(node); - - axe.commons.aria.lookupTable.role = {}; - var result = axe.commons.aria.implicitRole(node); - - assert.isNull(result); - }); -}); diff --git a/test/commons/text/subtree-text.js b/test/commons/text/subtree-text.js index ca5fbd2a37..1f3c6c4944 100644 --- a/test/commons/text/subtree-text.js +++ b/test/commons/text/subtree-text.js @@ -1,6 +1,8 @@ describe('text.subtreeText', function() { var fixtureSetup = axe.testUtils.fixtureSetup; var subtreeText = axe.commons.text.subtreeText; + var flatTreeSetup = axe.testUtils.flatTreeSetup; + var fixture = document.querySelector('#fixture'); it('concatinated the accessible name for child elements', function() { fixtureSetup('foo bar baz'); @@ -40,6 +42,7 @@ describe('text.subtreeText', function() { var h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; var text = h1.children[0]; var context = { processed: [] }; + flatTreeSetup(fixture); subtreeText(h1, context); assert.deepEqual(context.processed, [h1, text]); }); @@ -48,12 +51,14 @@ describe('text.subtreeText', function() { var h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; var text = h1.children[0]; var emptyContext = {}; + flatTreeSetup(fixture); subtreeText(h1, emptyContext); assert.deepEqual(emptyContext.processed, [h1, text]); }); it('returns `` when the element is in the `processed` array', function() { var h1 = axe.utils.querySelectorAll(axe._tree[0], 'h1')[0]; + flatTreeSetup(fixture); var context = { processed: [h1] }; diff --git a/test/integration/api/external/index.js b/test/integration/api/external/index.js index e6b3268b92..0e359bcf52 100644 --- a/test/integration/api/external/index.js +++ b/test/integration/api/external/index.js @@ -40,6 +40,7 @@ describe('external API', function() { describe('axe.commons.aria.implicitRole', function() { it('must be a function with the signature Element -> String|null', function() { + axe.utils.getFlattenedTree(document.documentElement); var implicitRolesOrNull = getEntries( axe.commons.aria.lookupTable.role ).reduce( diff --git a/test/rule-matches/heading-matches.js b/test/rule-matches/heading-matches.js index 8ea4e56699..319e124c22 100644 --- a/test/rule-matches/heading-matches.js +++ b/test/rule-matches/heading-matches.js @@ -2,6 +2,7 @@ describe('heading-matches', function() { 'use strict'; var fixture = document.getElementById('fixture'); + var flatTreeSetup = axe.testUtils.flatTreeSetup; var rule; beforeEach(function() { @@ -20,12 +21,16 @@ describe('heading-matches', function() { it('should return false on elements that are not headings', function() { var div = document.createElement('div'); + fixture.appendChild(div); + flatTreeSetup(fixture); assert.isFalse(rule.matches(div)); }); it('should return true on elements with "heading" in the role', function() { var div = document.createElement('div'); div.setAttribute('role', 'heading'); + fixture.appendChild(div); + flatTreeSetup(fixture); assert.isTrue(rule.matches(div)); div.setAttribute('role', 'slider heading'); @@ -36,6 +41,12 @@ describe('heading-matches', function() { var h1 = document.createElement('h1'); var h2 = document.createElement('h2'); var h3 = document.createElement('h3'); + + fixture.appendChild(h1); + fixture.appendChild(h2); + fixture.appendChild(h3); + + flatTreeSetup(fixture); assert.isTrue(rule.matches(h1)); assert.isTrue(rule.matches(h2)); assert.isTrue(rule.matches(h3)); @@ -44,18 +55,24 @@ describe('heading-matches', function() { it('should return false on headings with their role changes', function() { var h1 = document.createElement('h1'); h1.setAttribute('role', 'banner'); + fixture.appendChild(h1); + flatTreeSetup(fixture); assert.isFalse(rule.matches(h1)); }); it('should return true on headings with their role changes to an invalid role', function() { var h1 = document.createElement('h1'); h1.setAttribute('role', 'bruce'); + fixture.appendChild(h1); + flatTreeSetup(fixture); assert.isTrue(rule.matches(h1)); }); it('should return true on headings with their role changes to an abstract role', function() { var h1 = document.createElement('h1'); h1.setAttribute('role', 'widget'); + fixture.appendChild(h1); + flatTreeSetup(fixture); assert.isTrue(rule.matches(h1)); }); });