From 30efbfffc8b83ca03b4b2697e4202027826195f9 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 12 Jun 2020 08:33:01 -0600 Subject: [PATCH] feat(matches): add explicitRole, implicitRole, and semanticRole matches functions (#2286) --- lib/commons/matches/attributes.js | 10 ++-- lib/commons/matches/explicit-role.js | 24 +++++++++ lib/commons/matches/from-definition.js | 18 ++++--- lib/commons/matches/implicit-role.js | 24 +++++++++ lib/commons/matches/index.js | 6 +++ lib/commons/matches/node-name.js | 10 ++-- lib/commons/matches/properties.js | 10 ++-- lib/commons/matches/semantic-role.js | 24 +++++++++ test/commons/matches/explicit-role.js | 41 ++++++++++++++ test/commons/matches/from-definition.js | 72 +++++++++++++++++++++++++ test/commons/matches/implicit-role.js | 43 +++++++++++++++ test/commons/matches/semantic-role.js | 41 ++++++++++++++ 12 files changed, 298 insertions(+), 25 deletions(-) create mode 100644 lib/commons/matches/explicit-role.js create mode 100644 lib/commons/matches/implicit-role.js create mode 100644 lib/commons/matches/semantic-role.js create mode 100644 test/commons/matches/explicit-role.js create mode 100644 test/commons/matches/implicit-role.js create mode 100644 test/commons/matches/semantic-role.js diff --git a/lib/commons/matches/attributes.js b/lib/commons/matches/attributes.js index c5a3a1f0eb..0445265473 100644 --- a/lib/commons/matches/attributes.js +++ b/lib/commons/matches/attributes.js @@ -1,4 +1,6 @@ import fromFunction from './from-function'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; /** * Check if a virtual node matches some attribute(s) @@ -21,12 +23,8 @@ import fromFunction from './from-function'; * @returns {Boolean} */ function attributes(vNode, matcher) { - // TODO: this is a ridiculous hack since webpack is making these two - // separate functions - // TODO: es-module-AbstractVirtualNode - if (!axe._isAbstractNode(vNode)) { - // TODO: es-module-utils.getNodeFromTree - vNode = axe.utils.getNodeFromTree(vNode); + if (!(vNode instanceof AbstractVirtualNode)) { + vNode = getNodeFromTree(vNode); } return fromFunction(attrName => vNode.attr(attrName), matcher); } diff --git a/lib/commons/matches/explicit-role.js b/lib/commons/matches/explicit-role.js new file mode 100644 index 0000000000..48d32aad7a --- /dev/null +++ b/lib/commons/matches/explicit-role.js @@ -0,0 +1,24 @@ +import fromPrimative from './from-primative'; +import getRole from '../aria/get-role'; + +/** + * Check if a virtual node matches an explicit role(s) + *`` + * Note: matches.explicitRole(vNode, matcher) can be indirectly used through + * matches(vNode, { explicitRole: matcher }) + * + * Example: + * ```js + * matches.explicitRole(vNode, ['combobox', 'textbox']); + * matches.explicitRole(vNode, 'combobox'); + * ``` + * + * @param {VirtualNode} vNode + * @param {Object} matcher + * @returns {Boolean} + */ +function explicitRole(vNode, matcher) { + return fromPrimative(getRole(vNode, { noImplicit: true }), matcher); +} + +export default explicitRole; diff --git a/lib/commons/matches/from-definition.js b/lib/commons/matches/from-definition.js index 3c6b41a686..11c6ded690 100644 --- a/lib/commons/matches/from-definition.js +++ b/lib/commons/matches/from-definition.js @@ -1,13 +1,21 @@ import attributes from './attributes'; import condition from './condition'; +import explicitRole from './explicit-role'; +import implicitRole from './implicit-role'; import nodeName from './node-name'; import properties from './properties'; +import semanticRole from './semantic-role'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; const matchers = { attributes, condition, + explicitRole, + implicitRole, nodeName, - properties + properties, + semanticRole }; /** @@ -34,12 +42,8 @@ const matchers = { * @returns {Boolean} */ function fromDefinition(vNode, definition) { - // TODO: this is a ridiculous hack since webpack is making these two - // separate functions - // TODO: es-module-AbstractVirtualNode - if (!axe._isAbstractNode(vNode)) { - // TODO: es-module-utils.getNodeFromTree - vNode = axe.utils.getNodeFromTree(vNode); + if (!(vNode instanceof AbstractVirtualNode)) { + vNode = getNodeFromTree(vNode); } if (Array.isArray(definition)) { diff --git a/lib/commons/matches/implicit-role.js b/lib/commons/matches/implicit-role.js new file mode 100644 index 0000000000..2ff949ceae --- /dev/null +++ b/lib/commons/matches/implicit-role.js @@ -0,0 +1,24 @@ +import fromPrimative from './from-primative'; +import getImplicitRole from '../aria/implicit-role'; + +/** + * Check if a virtual node matches an implicit role(s) + *`` + * Note: matches.implicitRole(vNode, matcher) can be indirectly used through + * matches(vNode, { implicitRole: matcher }) + * + * Example: + * ```js + * matches.implicitRole(vNode, ['combobox', 'textbox']); + * matches.implicitRole(vNode, 'combobox'); + * ``` + * + * @param {VirtualNode} vNode + * @param {Object} matcher + * @returns {Boolean} + */ +function implicitRole(vNode, matcher) { + return fromPrimative(getImplicitRole(vNode.actualNode), matcher); +} + +export default implicitRole; diff --git a/lib/commons/matches/index.js b/lib/commons/matches/index.js index ad0d39a9b3..f0434676fc 100644 --- a/lib/commons/matches/index.js +++ b/lib/commons/matches/index.js @@ -5,19 +5,25 @@ */ import attributes from './attributes'; import condition from './condition'; +import explicitRole from './explicit-role'; import fromDefinition from './from-definition'; import fromFunction from './from-function'; import fromPrimative from './from-primative'; +import implicitRole from './implicit-role'; import matches from './matches'; import nodeName from './node-name'; import properties from './properties'; +import semanticRole from './semantic-role'; matches.attributes = attributes; matches.condition = condition; +matches.explicitRole = explicitRole; matches.fromDefinition = fromDefinition; matches.fromFunction = fromFunction; matches.fromPrimative = fromPrimative; +matches.implicitRole = implicitRole; matches.nodeName = nodeName; matches.properties = properties; +matches.semanticRole = semanticRole; export default matches; diff --git a/lib/commons/matches/node-name.js b/lib/commons/matches/node-name.js index ba76eb248c..f4ac42c721 100644 --- a/lib/commons/matches/node-name.js +++ b/lib/commons/matches/node-name.js @@ -1,4 +1,6 @@ import fromPrimative from './from-primative'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; /** * Check if the nodeName of a virtual node matches some value. @@ -18,12 +20,8 @@ import fromPrimative from './from-primative'; * @returns {Boolean} */ function nodeName(vNode, matcher) { - // TODO: this is a ridiculous hack since webpack is making these two - // separate functions - // TODO: es-module-AbstractVirtualNode - if (!axe._isAbstractNode(vNode)) { - // TODO: es-module-utils.getNodeFromTree - vNode = axe.utils.getNodeFromTree(vNode); + if (!(vNode instanceof AbstractVirtualNode)) { + vNode = getNodeFromTree(vNode); } return fromPrimative(vNode.props.nodeName, matcher); } diff --git a/lib/commons/matches/properties.js b/lib/commons/matches/properties.js index 7d394f3303..2220a16f38 100644 --- a/lib/commons/matches/properties.js +++ b/lib/commons/matches/properties.js @@ -1,4 +1,6 @@ import fromFunction from './from-function'; +import AbstractVirtualNode from '../../core/base/virtual-node/abstract-virtual-node'; +import { getNodeFromTree } from '../../core/utils'; /** * Check if a virtual node matches some attribute(s) @@ -21,12 +23,8 @@ import fromFunction from './from-function'; * @returns {Boolean} */ function properties(vNode, matcher) { - // TODO: this is a ridiculous hack since webpack is making these two - // separate functions - // TODO: es-module-AbstractVirtualNode - if (!axe._isAbstractNode(vNode)) { - // TODO: es-module-utils.getNodeFromTree - vNode = axe.utils.getNodeFromTree(vNode); + if (!(vNode instanceof AbstractVirtualNode)) { + vNode = getNodeFromTree(vNode); } return fromFunction(propName => vNode.props[propName], matcher); } diff --git a/lib/commons/matches/semantic-role.js b/lib/commons/matches/semantic-role.js new file mode 100644 index 0000000000..2ffe6c8f0b --- /dev/null +++ b/lib/commons/matches/semantic-role.js @@ -0,0 +1,24 @@ +import fromPrimative from './from-primative'; +import getRole from '../aria/get-role'; + +/** + * Check if a virtual node matches an semantic role(s) + *`` + * Note: matches.semanticRole(vNode, matcher) can be indirectly used through + * matches(vNode, { semanticRole: matcher }) + * + * Example: + * ```js + * matches.semanticRole(vNode, ['combobox', 'textbox']); + * matches.semanticRole(vNode, 'combobox'); + * ``` + * + * @param {VirtualNode} vNode + * @param {Object} matcher + * @returns {Boolean} + */ +function semanticRole(vNode, matcher) { + return fromPrimative(getRole(vNode), matcher); +} + +export default semanticRole; diff --git a/test/commons/matches/explicit-role.js b/test/commons/matches/explicit-role.js new file mode 100644 index 0000000000..09446b54e4 --- /dev/null +++ b/test/commons/matches/explicit-role.js @@ -0,0 +1,41 @@ +describe('matches.explicitRole', function() { + var explicitRole = axe.commons.matches.explicitRole; + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + + beforeEach(function() { + fixture.innerHTML = ''; + }); + + it('should return true if explicit role matches', function() { + var virtualNode = queryFixture(''); + assert.isTrue(explicitRole(virtualNode, 'textbox')); + }); + + it('should return true if explicit role matches array', function() { + var virtualNode = queryFixture(''); + assert.isTrue(explicitRole(virtualNode, ['combobox', 'textbox'])); + }); + + it('should return false if explicit role does not match', function() { + var virtualNode = queryFixture(''); + assert.isFalse(explicitRole(virtualNode, 'textbox')); + }); + + it('should return false if matching implicit role', function() { + var virtualNode = queryFixture(''); + assert.isFalse(explicitRole(virtualNode, 'listitem')); + }); + + // TODO: will only work when get-role works exclusively with virtual + // nodes + it.skip('works with SerialVirtualNode', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'span', + attributes: { + role: 'textbox' + } + }); + assert.isTrue(explicitRole(serialNode, 'textbox')); + }); +}); diff --git a/test/commons/matches/from-definition.js b/test/commons/matches/from-definition.js index bd3c460647..ee84a5d89b 100644 --- a/test/commons/matches/from-definition.js +++ b/test/commons/matches/from-definition.js @@ -93,6 +93,78 @@ describe('matches.fromDefinition', function() { ); }); + it('matches a definition with an `explicitRole` property', function() { + var virtualNode = queryFixture(''); + var matchers = [ + 'textbox', + ['textbox', 'combobox'], + /textbox/, + function(attributeName) { + return attributeName === 'textbox'; + } + ]; + matchers.forEach(function(matcher) { + assert.isTrue( + fromDefinition(virtualNode, { + explicitRole: matcher + }) + ); + }); + assert.isFalse( + fromDefinition(virtualNode, { + explicitRole: 'main' + }) + ); + }); + + it('matches a definition with an `implicitRole` property', function() { + var virtualNode = queryFixture(''); + var matchers = [ + 'textbox', + ['textbox', 'combobox'], + /textbox/, + function(attributeName) { + return attributeName === 'textbox'; + } + ]; + matchers.forEach(function(matcher) { + assert.isTrue( + fromDefinition(virtualNode, { + implicitRole: matcher + }) + ); + }); + assert.isFalse( + fromDefinition(virtualNode, { + implicitRole: 'main' + }) + ); + }); + + it('matches a definition with an `semanticRole` property', function() { + var virtualNode = queryFixture(''); + var matchers = [ + 'textbox', + ['textbox', 'combobox'], + /textbox/, + function(attributeName) { + return attributeName === 'textbox'; + } + ]; + matchers.forEach(function(matcher) { + assert.isTrue( + fromDefinition(virtualNode, { + semanticRole: matcher + }) + ); + }); + assert.isFalse( + fromDefinition(virtualNode, { + semanticRole: 'main' + }) + ); + }); + it('returns true when all matching properties return true', function() { var virtualNode = queryFixture( '' diff --git a/test/commons/matches/implicit-role.js b/test/commons/matches/implicit-role.js new file mode 100644 index 0000000000..a5a5151900 --- /dev/null +++ b/test/commons/matches/implicit-role.js @@ -0,0 +1,43 @@ +describe('matches.implicitRole', function() { + var implicitRole = axe.commons.matches.implicitRole; + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + + beforeEach(function() { + fixture.innerHTML = ''; + }); + + it('should return true if implicit role matches', function() { + var virtualNode = queryFixture(''); + assert.isTrue(implicitRole(virtualNode, 'listitem')); + }); + + it('should return true if implicit role matches array', function() { + var virtualNode = queryFixture(''); + assert.isTrue(implicitRole(virtualNode, ['textbox', 'listitem'])); + }); + + it('should return false if implicit role does not match', function() { + var virtualNode = queryFixture(''); + assert.isFalse(implicitRole(virtualNode, 'textbox')); + }); + + it('should return false if matching explicit role', function() { + var virtualNode = queryFixture( + '' + ); + assert.isFalse(implicitRole(virtualNode, 'menuitem')); + }); + + // TODO: will only work when get-role works exclusively with virtual + // nodes + it.skip('works with SerialVirtualNode', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'span', + attributes: { + role: 'textbox' + } + }); + assert.isTrue(implicitRole(serialNode, 'textbox')); + }); +}); diff --git a/test/commons/matches/semantic-role.js b/test/commons/matches/semantic-role.js new file mode 100644 index 0000000000..e3b6b57005 --- /dev/null +++ b/test/commons/matches/semantic-role.js @@ -0,0 +1,41 @@ +describe('matches.semanticRole', function() { + var semanticRole = axe.commons.matches.semanticRole; + var fixture = document.querySelector('#fixture'); + var queryFixture = axe.testUtils.queryFixture; + + beforeEach(function() { + fixture.innerHTML = ''; + }); + + it('should return true if explicit role matches', function() { + var virtualNode = queryFixture(''); + assert.isTrue(semanticRole(virtualNode, 'textbox')); + }); + + it('should return true if explicit role matches array', function() { + var virtualNode = queryFixture(''); + assert.isTrue(semanticRole(virtualNode, ['combobox', 'textbox'])); + }); + + it('should return true if implicit role matches', function() { + var virtualNode = queryFixture(''); + assert.isTrue(semanticRole(virtualNode, 'listitem')); + }); + + it('should return false if semantic role does not match', function() { + var virtualNode = queryFixture(''); + assert.isFalse(semanticRole(virtualNode, 'textbox')); + }); + + // TODO: will only work when get-role works exclusively with virtual + // nodes + it.skip('works with SerialVirtualNode', function() { + var serialNode = new axe.SerialVirtualNode({ + nodeName: 'span', + attributes: { + role: 'textbox' + } + }); + assert.isTrue(semanticRole(serialNode, 'textbox')); + }); +});