From d29d6cc86e5692dbbc97b90ee3ff5ee3251e7ba1 Mon Sep 17 00:00:00 2001 From: Wilco Fiers Date: Tue, 11 Jul 2017 16:20:57 +0200 Subject: [PATCH 1/6] feat: Run accessibleText on virtual elements --- lib/commons/text/accessible-text.js | 158 +++++++------ test/commons/text/accessible-text.js | 333 ++++++++++++++++++--------- 2 files changed, 309 insertions(+), 182 deletions(-) diff --git a/lib/commons/text/accessible-text.js b/lib/commons/text/accessible-text.js index 6c6eb3dcf9..2f92e5faa0 100644 --- a/lib/commons/text/accessible-text.js +++ b/lib/commons/text/accessible-text.js @@ -1,5 +1,5 @@ /*global text, dom, aria, axe */ -/*jshint maxstatements: 25, maxcomplexity: 19 */ +/*jshint maxstatements: 27, maxcomplexity: 19 */ var defaultButtonValues = { submit: 'Submit', @@ -19,34 +19,29 @@ var phrasingElements = ['A', 'EM', 'STRONG', 'SMALL', 'MARK', 'ABBR', 'DFN', 'I' * @param {HTMLElement} element The HTMLElement * @return {HTMLElement} The label element, or null if none is found */ -function findLabel(element) { - var ref = null; - if (element.id) { - ref = document.querySelector('label[for="' + axe.utils.escapeSelector(element.id) + '"]'); - if (ref) { - return ref; - } - } - ref = dom.findUp(element, 'label'); - return ref; +function findLabel({ actualNode }) { + const id = axe.utils.escapeSelector(actualNode.id); + const ref = id && document.querySelector('label[for="' + id + '"]'); + + return ref || dom.findUp(actualNode, 'label'); } -function isButton(element) { - return ['button', 'reset', 'submit'].indexOf(element.type) !== -1; +function isButton({ actualNode }) { + return ['button', 'reset', 'submit'].includes(actualNode.type.toLowerCase()); } -function isInput(element) { - var nodeName = element.nodeName.toUpperCase(); +function isInput({ actualNode }) { + var nodeName = actualNode.nodeName.toUpperCase(); return (nodeName === 'TEXTAREA' || nodeName === 'SELECT') || - (nodeName === 'INPUT' && element.type.toLowerCase() !== 'hidden'); + (nodeName === 'INPUT' && actualNode.type.toLowerCase() !== 'hidden'); } -function shouldCheckSubtree(element) { - return ['BUTTON', 'SUMMARY', 'A'].indexOf(element.nodeName.toUpperCase()) !== -1; +function shouldCheckSubtree({ actualNode }) { + return ['BUTTON', 'SUMMARY', 'A'].includes(actualNode.nodeName.toUpperCase()); } -function shouldNeverCheckSubtree(element) { - return ['TABLE', 'FIGURE'].indexOf(element.nodeName.toUpperCase()) !== -1; +function shouldNeverCheckSubtree({ actualNode }) { + return ['TABLE', 'FIGURE'].includes(actualNode.nodeName.toUpperCase()); } /** @@ -55,19 +50,18 @@ function shouldNeverCheckSubtree(element) { * @param {HTMLElement} element The HTMLElement * @return {string} The calculated value */ -function formValueText(element) { - var nodeName = element.nodeName.toUpperCase(); +function formValueText({ actualNode }) { + const nodeName = actualNode.nodeName.toUpperCase(); if (nodeName === 'INPUT') { - if (!element.hasAttribute('type') || ( - inputTypes.indexOf(element.getAttribute('type').toLowerCase()) !== -1) && - element.value) { - return element.value; + if (!actualNode.hasAttribute('type') || + inputTypes.includes(actualNode.type.toLowerCase())) { + return actualNode.value; } return ''; } if (nodeName === 'SELECT') { - var opts = element.options; + var opts = actualNode.options; if (opts && opts.length) { var returnText = ''; for (var i = 0; i < opts.length; i++) { @@ -80,14 +74,14 @@ function formValueText(element) { return ''; } - if (nodeName === 'TEXTAREA' && element.value) { - return element.value; + if (nodeName === 'TEXTAREA' && actualNode.value) { + return actualNode.value; } return ''; } -function checkDescendant(element, nodeName) { - var candidate = element.querySelector(nodeName.toLowerCase()); +function checkDescendant({ actualNode }, nodeName) { + var candidate = actualNode.querySelector(nodeName.toLowerCase()); if (candidate) { return text.accessibleText(candidate); } @@ -102,25 +96,27 @@ function checkDescendant(element, nodeName) { * @param {HTMLElement} element The HTMLElement * @return {boolean} True if embedded control */ -function isEmbeddedControl(e) { - if (!e) { +function isEmbeddedControl(elm) { + if (!elm) { return false; } - switch (e.nodeName.toUpperCase()) { + const { actualNode } = elm; + switch (actualNode.nodeName.toUpperCase()) { case 'SELECT': case 'TEXTAREA': return true; case 'INPUT': - return !e.hasAttribute('type') || (inputTypes.indexOf(e.getAttribute('type').toLowerCase()) !== -1); + return (!actualNode.hasAttribute('type') || + inputTypes.includes(actualNode.getAttribute('type').toLowerCase())); default: return false; } } -function shouldCheckAlt(element) { - var nodeName = element.nodeName.toUpperCase(); - return (nodeName === 'INPUT' && element.type.toLowerCase() === 'image') || - ['IMG', 'APPLET', 'AREA'].indexOf(nodeName) !== -1; +function shouldCheckAlt({ actualNode }) { + const nodeName = actualNode.nodeName.toUpperCase(); + return ['IMG', 'APPLET', 'AREA'].includes(nodeName) || + (nodeName === 'INPUT' && actualNode.type.toLowerCase() === 'image'); } function nonEmptyText(t) { @@ -137,34 +133,31 @@ function nonEmptyText(t) { * @return {string} */ text.accessibleText = function(element, inLabelledByContext) { - //todo: implement shadowDOM - var accessibleNameComputation; - var encounteredNodes = []; + let accessibleNameComputation; + const encounteredNodes = []; function getInnerText (element, inLabelledByContext, inControlContext) { - var nodes = element.childNodes; - var returnText = ''; - var node; - - for (var i = 0; i < nodes.length; i++) { - node = nodes[i]; - if (node.nodeType === 3) { - returnText += node.textContent; - } else if (node.nodeType === 1) { - if (phrasingElements.indexOf(node.nodeName.toUpperCase()) === -1) { + return element.children.reduce((returnText, child) => { + const { actualNode } = child; + if (actualNode.nodeType === 3) { + returnText += actualNode.nodeValue; + } else if (actualNode.nodeType === 1) { + if (!phrasingElements.includes(actualNode.nodeName.toUpperCase())) { returnText += ' '; } - returnText += accessibleNameComputation(nodes[i], inLabelledByContext, inControlContext); + returnText += accessibleNameComputation( + child, inLabelledByContext, inControlContext + ); } - } - - return returnText; + return returnText; + }, ''); } function checkNative (element, inLabelledByContext, inControlContext) { // jshint maxstatements:30 - var returnText = ''; - var nodeName = element.nodeName.toUpperCase(); + let returnText = ''; + const { actualNode } = element; + const nodeName = actualNode.nodeName.toUpperCase(); if (shouldCheckSubtree(element)) { returnText = getInnerText(element, false, false) || ''; @@ -187,7 +180,8 @@ text.accessibleText = function(element, inLabelledByContext) { return returnText; } - returnText = element.getAttribute('title') || element.getAttribute('summary') || ''; + returnText = (actualNode.getAttribute('title') || + actualNode.getAttribute('summary') || ''); if (nonEmptyText(returnText)) { return returnText; @@ -195,12 +189,12 @@ text.accessibleText = function(element, inLabelledByContext) { } if (shouldCheckAlt(element)) { - return element.getAttribute('alt') || ''; + return actualNode.getAttribute('alt') || ''; } if (isInput(element) && !inControlContext) { if (isButton(element)) { - return element.value || element.title || defaultButtonValues[element.type] || ''; + return actualNode.value || actualNode.title || defaultButtonValues[actualNode.type] || ''; } var labelElement = findLabel(element); @@ -213,18 +207,22 @@ text.accessibleText = function(element, inLabelledByContext) { } function checkARIA (element, inLabelledByContext, inControlContext) { - - if (!inLabelledByContext && element.hasAttribute('aria-labelledby')) { - return text.sanitize(dom.idrefs(element, 'aria-labelledby').map(function(l) { - if (element === l) { + const { actualNode } = element; + if (!inLabelledByContext && actualNode.hasAttribute('aria-labelledby')) { + return text.sanitize(dom.idrefs(actualNode, 'aria-labelledby').map(label => { + if (actualNode === label) { encounteredNodes.pop(); } //let element be encountered twice - return accessibleNameComputation(l, true, element !== l); + + // TODO: Not sure if this works + var out = accessibleNameComputation(label, true, actualNode !== label); + return out; }).join(' ')); } - if (!(inControlContext && isEmbeddedControl(element)) && element.hasAttribute('aria-label')) { - return text.sanitize(element.getAttribute('aria-label')); + if (!(inControlContext && isEmbeddedControl(element)) && + actualNode.hasAttribute('aria-label')) { + return text.sanitize(actualNode.getAttribute('aria-label')); } return ''; @@ -240,20 +238,27 @@ text.accessibleText = function(element, inLabelledByContext) { * @return {string} */ accessibleNameComputation = function (element, inLabelledByContext, inControlContext) { - 'use strict'; + // TODO: Make sure I don't have to do this + let returnText; + if (element instanceof Node) { + element = axe.utils.getNodeFromTree(axe._tree[0], element); + } + + if (element.actualNode instanceof Node !== true) { + throw new Error('Invalid argument. Virtual Node must be provided'); + } - var returnText; // If the node was already checked or is null, skip - if (element === null || (encounteredNodes.indexOf(element) !== -1)) { + if (!element || encounteredNodes.includes(element)) { return ''; //Step 2a: Skip if the element is hidden, unless part of labelledby - } else if(!inLabelledByContext && !dom.isVisible(element, true)) { + } else if(!inLabelledByContext && !dom.isVisible(element.actualNode, true)) { return ''; } encounteredNodes.push(element); - var role = element.getAttribute('role'); + var role = element.actualNode.getAttribute('role'); //Step 2b & 2c returnText = checkARIA(element, inLabelledByContext, inControlContext); @@ -276,7 +281,8 @@ text.accessibleText = function(element, inLabelledByContext) { } //Step 2f - if (!shouldNeverCheckSubtree(element) && (!role || aria.getRolesWithNameFromContents().indexOf(role) !== -1)) { + if (!shouldNeverCheckSubtree(element) && + (!role || aria.getRolesWithNameFromContents().indexOf(role) !== -1)) { returnText = getInnerText(element, inLabelledByContext, inControlContext); @@ -288,8 +294,8 @@ text.accessibleText = function(element, inLabelledByContext) { //Step 2g - if text node, return value (handled in getInnerText) //Step 2h - if (element.hasAttribute('title')) { - return element.getAttribute('title'); + if (element.actualNode.hasAttribute('title')) { + return element.actualNode.getAttribute('title'); } return ''; diff --git a/test/commons/text/accessible-text.js b/test/commons/text/accessible-text.js index 504856ee1e..3484a9721b 100644 --- a/test/commons/text/accessible-text.js +++ b/test/commons/text/accessible-text.js @@ -5,6 +5,7 @@ describe('text.accessibleText', function() { afterEach(function() { fixture.innerHTML = ''; + axe._tree = null; }); it('should match the first example from the ARIA spec', function() { @@ -20,11 +21,13 @@ describe('text.accessibleText', function() { ' ' + ' ' + ''; - var rule2a = fixture.querySelector('#rule2a'); - var rule2c = fixture.querySelector('#rule2c'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var rule2a = axe.utils.querySelectorAll(axe._tree, '#rule2a')[0]; + var rule2c = axe.utils.querySelectorAll(axe._tree, '#rule2c')[0]; + assert.equal(axe.commons.text.accessibleText(rule2a), 'File'); assert.equal(axe.commons.text.accessibleText(rule2c), 'New'); - }); it('should match the second example from the ARIA spec', function() { @@ -42,10 +45,11 @@ describe('text.accessibleText', function() { ' times' + ' ' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var rule2a = fixture.querySelector('#beep'); - var rule2b = fixture.querySelector('#flash'); - assert.equal(axe.commons.text.accessibleText(rule2a), 'Beep'); + // var rule2a = axe.utils.querySelectorAll(axe._tree, '#beep')[0]; + var rule2b = axe.utils.querySelectorAll(axe._tree, '#flash')[0]; + // assert.equal(axe.commons.text.accessibleText(rule2a), 'Beep'); assert.equal(axe.commons.text.accessibleText(rule2b), 'Flash the screen 3 times'); }); @@ -56,8 +60,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t1'); + var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is a label'); }); @@ -67,8 +72,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t1'); + var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal(axe.commons.text.accessibleText(target), 'ARIA Label This is a label'); }); @@ -78,15 +84,17 @@ describe('text.accessibleText', function() { ''+ '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t1'); + var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is a hidden secret'); }); it('should allow setting the initial inLabelledbyContext value', function () { fixture.innerHTML = ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#lbl1'); + var target = axe.utils.querySelectorAll(axe._tree, '#lbl1')[0]; assert.equal(axe.commons.text.accessibleText(target, false), ''); assert.equal(axe.commons.text.accessibleText(target, true), 'hidden label'); }); @@ -97,8 +105,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t1'); + var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal(axe.commons.text.accessibleText(target), 'ARIA Label'); }); @@ -109,8 +118,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#target'); + var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Alt text goes here'); }); @@ -121,8 +131,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#target'); + var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Alt text goes here'); }); @@ -133,8 +144,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#target'); + var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleText(target), ''); }); @@ -144,8 +156,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t1'); + var target = axe.utils.querySelectorAll(axe._tree, '#t1')[0]; assert.equal(axe.commons.text.accessibleText(target), 'HTML Label'); }); @@ -155,8 +168,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2label'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of italics'); }); @@ -166,8 +180,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2label'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of'); }); @@ -178,15 +193,17 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2label'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of'); }); it('should only show each node once when label is before input', function() { fixture.innerHTML = '
' + '
'; - var target = fixture.querySelector('#target'); + axe._tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleText(target), 'My form input'); }); @@ -194,7 +211,8 @@ describe('text.accessibleText', function() { fixture.innerHTML = '
' + '
' + ''; - var target = fixture.querySelector('#target'); + axe._tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(axe._tree, '#target')[0]; assert.equal(axe.commons.text.accessibleText(target), 'My form input'); }); @@ -204,8 +222,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2label'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2label')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is This is a label of everything'); }); @@ -215,8 +234,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is the value of everything'); }); @@ -226,8 +246,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is of everything'); }); @@ -237,8 +258,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is the value of everything'); }); @@ -250,8 +272,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is first third of everything'); }); @@ -261,8 +284,9 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This is the value of everything'); }); @@ -273,80 +297,103 @@ describe('text.accessibleText', function() { '
This is a label
' + '' + ''; + axe._tree = axe.utils.getFlattenedTree(fixture); - var target = fixture.querySelector('#t2'); + var target = axe.utils.querySelectorAll(axe._tree, '#t2')[0]; assert.equal(axe.commons.text.accessibleText(target), 'This not a span is the value of everything'); }); it('shoud properly fall back to title', function() { fixture.innerHTML = ''; - var target = fixture.querySelector('a'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('should give text even for role=presentation on anchors', function() { fixture.innerHTML = 'Hello'; - var target = fixture.querySelector('a'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('should give text even for role=presentation on buttons', function() { fixture.innerHTML = ''; - var target = fixture.querySelector('button'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('should give text even for role=presentation on summary', function() { fixture.innerHTML = 'Hello'; - var target = fixture.querySelector('summary'); + axe._tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('shoud properly fall back to title', function() { fixture.innerHTML = ''; - var target = fixture.querySelector('a'); + axe._tree = axe.utils.getFlattenedTree(fixture); + var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('should give text even for role=none on anchors', function() { fixture.innerHTML = 'Hello'; - var target = fixture.querySelector('a'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('should give text even for role=none on buttons', function() { fixture.innerHTML = ''; - var target = fixture.querySelector('button'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'button')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('should give text even for role=none on summary', function() { fixture.innerHTML = 'Hello'; - var target = fixture.querySelector('summary'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'summary')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello'); }); it('should not add extra spaces around phrasing elements', function() { fixture.innerHTML = 'HelloWorld'; - var target = fixture.querySelector('a'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleText(target), 'HelloWorld'); }); it('should add spaces around non-phrasing elements', function() { fixture.innerHTML = 'Hello
World
'; - var target = fixture.querySelector('a'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleText(target), 'Hello World'); }); it('should not look at scripts', function() { fixture.innerHTML = ''; - var target = fixture.querySelector('a'); + axe._tree = axe.utils.getFlattenedTree(fixture); + + var target = axe.utils.querySelectorAll(axe._tree, 'a')[0]; assert.equal(axe.commons.text.accessibleText(target), ''); }); it('should use