From e8e17e42c3594518ee60838749a507504a839c69 Mon Sep 17 00:00:00 2001
From: Steven Lambert <2433219+straker@users.noreply.github.com>
Date: Fri, 17 Jul 2020 08:31:41 -0600
Subject: [PATCH] feat(object-alt,accessible-text): object-alt rule and
accessible text to work with serial virtual nodes with children
* feat(object-alt,accessible-text,native-element-type): object-alt rule and accessible text to work with serial virtual nodes with children. deprecate native-element-type
* remove #text from html-elm
* nodeValue
* fix tests
* fix test
* remove file
* rename
---
lib/commons/aria/get-owned-virtual.js | 20 +--
lib/commons/aria/lookup-table.js | 5 +-
lib/commons/standards/get-element-spec.js | 5 +
lib/commons/text/accessible-text-virtual.js | 21 ++-
lib/commons/text/form-control-value.js | 2 +-
lib/commons/text/native-text-alternative.js | 13 +-
lib/commons/text/subtree-text.js | 82 ++++-----
.../virtual-node/abstract-virtual-node.js | 1 -
.../base/virtual-node/serial-virtual-node.js | 4 +-
lib/core/base/virtual-node/virtual-node.js | 5 +-
lib/core/public/run-virtual-rule.js | 5 +-
lib/standards/html-elms.js | 19 +--
.../checks/aria/no-implicit-explicit-label.js | 4 +-
test/checks/shared/button-has-visible-text.js | 43 ++++-
test/checks/shared/has-visible-text.js | 29 +++-
test/commons/standards/get-element-spec.js | 5 +
.../base/virtual-node/serial-virtual-node.js | 12 +-
test/integration/virtual-rules/index.html | 2 +-
test/integration/virtual-rules/object-alt.js | 156 ++++++++++++++++++
19 files changed, 335 insertions(+), 98 deletions(-)
create mode 100644 test/integration/virtual-rules/object-alt.js
diff --git a/lib/commons/aria/get-owned-virtual.js b/lib/commons/aria/get-owned-virtual.js
index 8c27f0d2eb..53c471b7a9 100644
--- a/lib/commons/aria/get-owned-virtual.js
+++ b/lib/commons/aria/get-owned-virtual.js
@@ -6,22 +6,22 @@ import idrefs from '../dom/idrefs';
* @param {VirtualNode} element
* @return {VirtualNode[]} Owned elements
*/
-function getOwnedVirtual({ actualNode, children }) {
- if (!actualNode || !children) {
+function getOwnedVirtual(virtualNode) {
+ const { actualNode, children } = virtualNode;
+ if (!children) {
throw new Error('getOwnedVirtual requires a virtual node');
}
// TODO: Check that the element has a role
// TODO: Descend into children with role=presentation|none
// TODO: Exclude descendents owned by other elements
+ if (virtualNode.hasAttr('aria-owns')) {
+ const owns = idrefs(actualNode, 'aria-owns')
+ .filter(element => !!element)
+ .map(element => axe.utils.getNodeFromTree(element));
+ return [...children, ...owns];
+ }
- return idrefs(actualNode, 'aria-owns').reduce((ownedElms, element) => {
- if (element) {
- // TODO: es-module-utils.getNodeFromTree
- const virtualNode = axe.utils.getNodeFromTree(element);
- ownedElms.push(virtualNode);
- }
- return ownedElms;
- }, children);
+ return [...children];
}
export default getOwnedVirtual;
diff --git a/lib/commons/aria/lookup-table.js b/lib/commons/aria/lookup-table.js
index d6b99ac968..1f703ced36 100644
--- a/lib/commons/aria/lookup-table.js
+++ b/lib/commons/aria/lookup-table.js
@@ -2178,10 +2178,7 @@ lookupTable.elementsAllowedNoRole = [
{
nodeName: 'select',
condition: vNode => {
- // TODO: this is a ridiculous hack since webpack is making these two
- // separate functions
- // TODO: es-module-AbstractVirtualNode
- if (!axe._isAbstractNode(vNode)) {
+ if (!(vNode instanceof axe.AbstractVirtualNode)) {
vNode = axe.utils.getNodeFromTree(vNode);
}
diff --git a/lib/commons/standards/get-element-spec.js b/lib/commons/standards/get-element-spec.js
index 13e6bb2022..4f578fe1ba 100644
--- a/lib/commons/standards/get-element-spec.js
+++ b/lib/commons/standards/get-element-spec.js
@@ -9,6 +9,11 @@ import matchesFn from '../../commons/matches';
function getElementSpec(vNode) {
const standard = standards.htmlElms[vNode.props.nodeName];
+ // invalid element name (could be an svg or custom element name)
+ if (!standard) {
+ return {};
+ }
+
if (!standard.variant) {
return standard;
}
diff --git a/lib/commons/text/accessible-text-virtual.js b/lib/commons/text/accessible-text-virtual.js
index bc742eb01f..7efa692535 100644
--- a/lib/commons/text/accessible-text-virtual.js
+++ b/lib/commons/text/accessible-text-virtual.js
@@ -32,7 +32,7 @@ function accessibleTextVirtual(virtualNode, context = {}) {
nativeTextAlternative, // Step 2D
formControlValue, // Step 2E
subtreeText, // Step 2F + Step 2H
- textNodeContent, // Step 2G (order with 2H does not matter)
+ textNodeValue, // Step 2G (order with 2H does not matter)
titleText // Step 2I
];
@@ -56,15 +56,15 @@ function accessibleTextVirtual(virtualNode, context = {}) {
}
/**
- * Return the textContent of a node
+ * Return the nodeValue of a node
* @param {VirtualNode} element
- * @return {String} textContent value
+ * @return {String} nodeValue value
*/
-function textNodeContent({ actualNode }) {
- if (actualNode.nodeType !== 3) {
+function textNodeValue(virtualNode) {
+ if (virtualNode.props.nodeType !== 3) {
return '';
}
- return actualNode.textContent;
+ return virtualNode.props.nodeValue;
}
/**
@@ -75,6 +75,10 @@ function textNodeContent({ actualNode }) {
* @return {Boolean}
*/
function shouldIgnoreHidden({ actualNode }, context) {
+ if (!actualNode) {
+ return false;
+ }
+
if (
// If the parent isn't ignored, the text node should not be either
actualNode.nodeType !== 1 ||
@@ -98,6 +102,11 @@ function prepareContext(virtualNode, context) {
if (!context.startNode) {
context = { startNode: virtualNode, ...context };
}
+
+ if (!actualNode) {
+ return context;
+ }
+
/**
* When `aria-labelledby` directly references a `hidden` element
* the element needs to be included in the accessible name.
diff --git a/lib/commons/text/form-control-value.js b/lib/commons/text/form-control-value.js
index c8ac64c436..a931933e70 100644
--- a/lib/commons/text/form-control-value.js
+++ b/lib/commons/text/form-control-value.js
@@ -43,7 +43,7 @@ export const formControlValueMethods = {
function formControlValue(virtualNode, context = {}) {
const { actualNode } = virtualNode;
const unsupportedRoles = unsupported.accessibleNameFromFieldValue || [];
- const role = getRole(actualNode);
+ const role = getRole(virtualNode);
if (
// For the targeted node, the accessible name is never the value:
diff --git a/lib/commons/text/native-text-alternative.js b/lib/commons/text/native-text-alternative.js
index 56102f4ce7..eb5f7e464c 100644
--- a/lib/commons/text/native-text-alternative.js
+++ b/lib/commons/text/native-text-alternative.js
@@ -1,6 +1,5 @@
import getRole from '../aria/get-role';
-import matches from '../matches/matches';
-import nativeElementType from './native-element-type';
+import getElementSpec from '../standards/get-element-spec';
import nativeTextMethods from './native-text-methods';
/**
@@ -13,7 +12,7 @@ import nativeTextMethods from './native-text-methods';
function nativeTextAlternative(virtualNode, context = {}) {
const { actualNode } = virtualNode;
if (
- actualNode.nodeType !== 1 ||
+ virtualNode.props.nodeType !== 1 ||
['presentation', 'none'].includes(getRole(virtualNode))
) {
return '';
@@ -38,12 +37,8 @@ function nativeTextAlternative(virtualNode, context = {}) {
* @return {Function[]} Array of native accessible name computation methods
*/
function findTextMethods(virtualNode) {
- const nativeType = nativeElementType.find(type => {
- return matches(virtualNode, type.matches);
- });
-
- // Use concat because namingMethods can be a string or an array of strings
- const methods = nativeType ? [].concat(nativeType.namingMethods) : [];
+ const elmSpec = getElementSpec(virtualNode);
+ const methods = elmSpec.namingMethods || [];
return methods.map(methodName => nativeTextMethods[methodName]);
}
diff --git a/lib/commons/text/subtree-text.js b/lib/commons/text/subtree-text.js
index 793c9ab637..971ef51365 100644
--- a/lib/commons/text/subtree-text.js
+++ b/lib/commons/text/subtree-text.js
@@ -29,50 +29,50 @@ function subtreeText(virtualNode, context = {}) {
// TODO: Could do with an "HTML" lookup table, similar to ARIA,
// where this sort of stuff can live.
const phrasingElements = [
- 'A',
- 'EM',
- 'STRONG',
- 'SMALL',
- 'MARK',
- 'ABBR',
- 'DFN',
- 'I',
- 'B',
- 'S',
- 'U',
- 'CODE',
- 'VAR',
- 'SAMP',
- 'KBD',
- 'SUP',
- 'SUB',
- 'Q',
- 'CITE',
- 'SPAN',
- 'BDO',
- 'BDI',
- 'WBR',
- 'INS',
- 'DEL',
- 'MAP',
- 'AREA',
- 'NOSCRIPT',
- 'RUBY',
- 'BUTTON',
- 'LABEL',
- 'OUTPUT',
- 'DATALIST',
- 'KEYGEN',
- 'PROGRESS',
- 'COMMAND',
- 'CANVAS',
- 'TIME',
- 'METER',
- '#TEXT'
+ '#text',
+ 'a',
+ 'abbr',
+ 'area',
+ 'b',
+ 'bdi',
+ 'bdo',
+ 'button',
+ 'canvas',
+ 'cite',
+ 'code',
+ 'command',
+ 'datalist',
+ 'del',
+ 'dfn',
+ 'em',
+ 'i',
+ 'ins',
+ 'kbd',
+ 'keygen',
+ 'label',
+ 'map',
+ 'mark',
+ 'meter',
+ 'noscript',
+ 'output',
+ 'progress',
+ 'q',
+ 'ruby',
+ 's',
+ 'samp',
+ 'small',
+ 'span',
+ 'strong',
+ 'sub',
+ 'sup',
+ 'time',
+ 'u',
+ 'var',
+ 'wbr'
];
function appendAccessibleText(contentText, virtualNode, context) {
- const nodeName = virtualNode.actualNode.nodeName.toUpperCase();
+ const nodeName = virtualNode.props.nodeName;
let contentTextAdd = accessibleTextVirtual(virtualNode, context);
if (!contentTextAdd) {
return contentText;
diff --git a/lib/core/base/virtual-node/abstract-virtual-node.js b/lib/core/base/virtual-node/abstract-virtual-node.js
index a661b3b5a3..63697a25a4 100644
--- a/lib/core/base/virtual-node/abstract-virtual-node.js
+++ b/lib/core/base/virtual-node/abstract-virtual-node.js
@@ -2,7 +2,6 @@ const whitespaceRegex = /[\t\r\n\f]/g;
class AbstractVirtualNode {
constructor() {
- this.children = [];
this.parent = null;
}
diff --git a/lib/core/base/virtual-node/serial-virtual-node.js b/lib/core/base/virtual-node/serial-virtual-node.js
index a94b8c9f7f..d28d915eb3 100644
--- a/lib/core/base/virtual-node/serial-virtual-node.js
+++ b/lib/core/base/virtual-node/serial-virtual-node.js
@@ -44,8 +44,8 @@ class SerialVirtualNode extends AbstractVirtualNode {
function normaliseProps(serialNode) {
let { nodeName, nodeType = 1 } = serialNode;
assert(
- nodeType === 1,
- `nodeType has to be undefined or 1, got '${nodeType}'`
+ typeof nodeType === 'number',
+ `nodeType has to be a number, got '${nodeType}'`
);
assert(
typeof nodeName === 'string',
diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js
index 7445efd08e..90473d273a 100644
--- a/lib/core/base/virtual-node/virtual-node.js
+++ b/lib/core/base/virtual-node/virtual-node.js
@@ -49,14 +49,15 @@ class VirtualNode extends AbstractVirtualNode {
// abstract Node properties so we can run axe in DOM-less environments.
// add to the prototype so memory is shared across all virtual nodes
get props() {
- const { nodeType, nodeName, id, multiple } = this.actualNode;
+ const { nodeType, nodeName, id, multiple, nodeValue } = this.actualNode;
return {
nodeType,
nodeName: this._isXHTML ? nodeName : nodeName.toLowerCase(),
id,
type: this._type,
- multiple
+ multiple,
+ nodeValue
};
}
diff --git a/lib/core/public/run-virtual-rule.js b/lib/core/public/run-virtual-rule.js
index 19d16a4e8c..1a3c292b16 100644
--- a/lib/core/public/run-virtual-rule.js
+++ b/lib/core/public/run-virtual-rule.js
@@ -14,10 +14,7 @@ function runVirtualRule(ruleId, vNode, options = {}) {
options.reporter = options.reporter || axe._audit.reporter || 'v1';
axe._selectorData = {};
- // TODO: this is a ridiculous hack since webpack is making these two
- // separate functions
- // TODO: es-module-AbstractVirtualNode
- if (!axe._isAbstractNode(vNode)) {
+ if (!(vNode instanceof axe.AbstractVirtualNode)) {
vNode = new axe.SerialVirtualNode(vNode);
}
diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js
index 3e6a0c2457..57d16f60d3 100644
--- a/lib/standards/html-elms.js
+++ b/lib/standards/html-elms.js
@@ -137,7 +137,7 @@ const htmlElms = {
'tab'
],
// 5.4 button Element
- namingMethods: 'subtreeText'
+ namingMethods: ['subtreeText']
},
canvas: {
allowedRoles: true,
@@ -220,7 +220,7 @@ const htmlElms = {
contentTypes: ['flow'],
allowedRoles: ['none', 'presentation', 'radiogroup'],
// 5.5 fieldset and legend Elements
- namingMethods: 'fieldsetLegendText'
+ namingMethods: ['fieldsetLegendText']
},
figcaption: {
allowedRoles: ['group', 'none', 'presentation']
@@ -359,7 +359,7 @@ const htmlElms = {
}
},
// 5.10 img Element
- namingMethods: 'altText'
+ namingMethods: ['altText']
},
input: {
variant: {
@@ -504,7 +504,7 @@ const htmlElms = {
},
// 5.1 input type="text", input type="password", input type="search", input type="tel", input type="url" and textarea Element
// 5.7 Other Form Elements
- namingMethods: 'labelText'
+ namingMethods: ['labelText']
}
}
},
@@ -512,7 +512,7 @@ const htmlElms = {
contentTypes: ['phrasing', 'flow'],
allowedRoles: true
},
- kdb: {
+ kbd: {
contentTypes: ['phrasing', 'flow'],
allowedRoles: true
},
@@ -647,10 +647,10 @@ const htmlElms = {
contentTypes: ['phrasing', 'flow'],
allowedRoles: true,
// 5.6 output Element
- namingMethods: 'subtreeText'
+ namingMethods: ['subtreeText']
},
p: {
- contentTypes: ['phrasing', 'flow'],
+ contentTypes: ['flow'],
allowedRoles: true,
shadowRoot: true
},
@@ -811,10 +811,9 @@ const htmlElms = {
allowedRoles: true
},
summary: {
- contentTypes: ['phrasing', 'flow'],
allowedRoles: false,
// 5.8 summary Element
- namingMethods: 'subtreeText'
+ namingMethods: ['subtreeText']
},
sup: {
contentTypes: ['phrasing', 'flow'],
@@ -841,7 +840,7 @@ const htmlElms = {
'aria-valuenow': '',
'aria-multiline': 'true'
},
- namingMethods: 'labelText'
+ namingMethods: ['labelText']
},
tfoot: {
allowedRoles: true
diff --git a/test/checks/aria/no-implicit-explicit-label.js b/test/checks/aria/no-implicit-explicit-label.js
index 88b8de3809..4873fc4a5b 100644
--- a/test/checks/aria/no-implicit-explicit-label.js
+++ b/test/checks/aria/no-implicit-explicit-label.js
@@ -44,7 +44,7 @@ describe('no-implicit-explicit-label', function() {
});
describe('SerialVirtualNode', function() {
- it('should return undefined', function() {
+ it('should return false', function() {
var serialNode = new axe.SerialVirtualNode({
nodeName: 'div',
attributes: {
@@ -54,7 +54,7 @@ describe('no-implicit-explicit-label', function() {
});
var actual = check.evaluate.call(checkContext, null, {}, serialNode);
- assert.isUndefined(actual);
+ assert.isFalse(actual);
});
});
});
diff --git a/test/checks/shared/button-has-visible-text.js b/test/checks/shared/button-has-visible-text.js
index 9c64c249fe..24d971cde7 100644
--- a/test/checks/shared/button-has-visible-text.js
+++ b/test/checks/shared/button-has-visible-text.js
@@ -54,13 +54,52 @@ describe('button-has-visible-text', function() {
});
describe('SerialVirtualNode', function() {
- it('should return undefined if no other attributes are provided', function() {
+ it('should return incomplete if no children are passed', function() {
var node = new axe.SerialVirtualNode({
nodeName: 'button'
});
assert.isUndefined(
- axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
+ axe.testUtils.getCheckEvaluate('button-has-visible-text')(
+ null,
+ {},
+ node
+ )
+ );
+ });
+
+ it('should return false if button element is empty', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'button'
+ });
+ node.children = [];
+
+ assert.isFalse(
+ axe.testUtils.getCheckEvaluate('button-has-visible-text')(
+ null,
+ {},
+ node
+ )
+ );
+ });
+
+ it('should return true if a button element has text', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'button'
+ });
+ var child = new axe.SerialVirtualNode({
+ nodeName: '#text',
+ nodeType: 3,
+ nodeValue: 'Text'
+ });
+ node.children = [child];
+
+ assert.isTrue(
+ axe.testUtils.getCheckEvaluate('button-has-visible-text')(
+ null,
+ {},
+ node
+ )
);
});
});
diff --git a/test/checks/shared/has-visible-text.js b/test/checks/shared/has-visible-text.js
index 342f77addb..174ea8fdfc 100644
--- a/test/checks/shared/has-visible-text.js
+++ b/test/checks/shared/has-visible-text.js
@@ -52,7 +52,7 @@ describe('has-visible-text', function() {
);
});
- it('should return undefined if element is named from contents', function() {
+ it('should return incomplete if no other properties are set', function() {
var node = new axe.SerialVirtualNode({
nodeName: 'button'
});
@@ -61,5 +61,32 @@ describe('has-visible-text', function() {
axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
);
});
+
+ it('should return false if there is no visible text', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'button'
+ });
+ node.children = [];
+
+ assert.isFalse(
+ axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
+ );
+ });
+
+ it('should return true if there is visible text', function() {
+ var node = new axe.SerialVirtualNode({
+ nodeName: 'object'
+ });
+ var child = new axe.SerialVirtualNode({
+ nodeName: '#text',
+ nodeType: 3,
+ nodeValue: 'hello!'
+ });
+ node.children = [child];
+
+ assert.isTrue(
+ axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node)
+ );
+ });
});
});
diff --git a/test/commons/standards/get-element-spec.js b/test/commons/standards/get-element-spec.js
index ba326a7d28..92dcc1fc7d 100644
--- a/test/commons/standards/get-element-spec.js
+++ b/test/commons/standards/get-element-spec.js
@@ -34,6 +34,11 @@ describe('standards.getElementSpec', function() {
});
});
+ it('should return empty object if passed an invalid element', function() {
+ var vNode = queryFixture('