Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(commons): Add matches methods #1270

Merged
merged 8 commits into from
Jan 3, 2019
Merged

feat(commons): Add matches methods #1270

merged 8 commits into from
Jan 3, 2019

Conversation

WilcoFiers
Copy link
Contributor

@WilcoFiers WilcoFiers commented Dec 9, 2018

This PR adds a series of "matches" functions to axe.commons.matches. These functions are designed to allow us to match elements on different types of things. We started doing this for the aria-allowed-role rule, but while working on the accessible name computation I found myself building a very similar thing.

This PR replaces the existing aria.validateNodeAndAttributes with a more general purpose axe.commons.matches function.

Note, I realise there is a bunch more refactoring that needs to happen. I'd prefer to keep that out of this PR, so that we can get the accessible name PR resolved.

Note This PR removes a commons that was introduced in 3.1.0. This is a breaking change. I do not think this will impact anyone, and so I didn't put in the time to put some deprecation thing around it, but if anyone thinks this is necessary I can put that in.

This PR is a prerequisite for #1163

Closes issue: none.

Reviewer checks

Required fields, to be filled out by PR reviewer(s)

  • Follows the commit message policy, appropriate for next version
  • Has documentation updated, a DU ticket, or requires no documentation change
  • Includes new tests, or was unnecessary
  • Code is reviewed for security by: @JKODU

@WilcoFiers WilcoFiers requested a review from a team as a code owner December 9, 2018 17:49
jeeyyy
jeeyyy previously requested changes Dec 10, 2018
Copy link
Contributor

@jeeyyy jeeyyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments, see inline.

Yet to review the tests.

I have also noticed that prettier has not run, for these commits. May be worth updating the branch, to be inline with develop.

function isNull (value) {
return value === null
}
function notNull (value) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isNotNull would be better naming, to go inline with isNull

elementConditions.MUST_HAVE_SIZE_ATTRIBUTE_WITH_VALUE_GREATER_THAN_1,
attributes: {
TYPE: 'MULTIPLE'
nodeName: 'select',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯 for all the nodeName changes as against tagName. Nice. 🎆

@@ -0,0 +1,8 @@
/* global matches */
matches.attributes = function matchesAttributes (node, matcher) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc comments.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where/how does this method get called?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can call it directly, or use it as a property on matches.fromDefinition() or .matches(). I'll add a comment into matches.fromDefinition like you suggested.


// Check that the property has all the expected values
return Object.keys(matcher).every(propName => {
return matches.fromString(getValue(propName), matcher[propName]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per this line - https://github.com/dequelabs/axe-core/pull/1270/files#diff-751a09d50f40e024172d208c3beb465bR5

the getValue(propName) sometimes can return an undefined if attribute is not defined.

It would be good to add some preventive code either in the function as an argument which is passed in as getValue, or in the fromString fn.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... array.includes(undefined) could be a problem I suppose. Let me think about it...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this addressed @WilcoFiers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, here:
https://github.com/dequelabs/axe-core/pull/1270/files#diff-3bbb4ce58de474cbc3dc418d3bf0de72R25

That's the only scenario where I expect difficulties.

'condition'
]

matches.fromDefinition = function matchFromDefinition (node, definition) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc comments.

return Object.keys(definition)
.every(matcherName => {
if (!matchers.includes(matcherName)) {
console.log(definition)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we surround this by the flag we have for logging in axe? debug: true?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't belong here it all. I should remove it. Good catch.

})
}

matches.condition = function (node, condition) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this used?

Also, perhaps worth having this in a separate file if there is a need for it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several "condition" things in the aria/index.js file. I didn't put it in its own file because I'm lazy, and it's a single line function. Didn't seem worth the effort. Please let me know if you think this needs to be done.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for separation of concerns, I would put this in a single file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely needs some comments about usage. Similar to matches.attributes, it's difficult to follow how the lookupTable functions with this matches object...

@@ -0,0 +1,15 @@
/* global matches */
matches.fromFunction = function matchFromFunction(getValue, matcher) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jsdoc comments

if (typeof isXHTMLGlobal === 'undefined') {
isXHTMLGlobal = axe.utils.isXHTML(node.ownerDocument);
}
if (typeof isXHTML === 'undefined') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The order of these two if's can be changed, so the need to follow a code path that executes probably only once is kept out of view.

There is no need to instantiate isXHTMLGlobal, if isXHTML is passed every time as an argument.

let isXHTMLGlobal;
matches.nodeName = function matchNodeName (node, matcher, { isXHTML = undefined } = {}) {
	node = node.actualNode || node;
	if (typeof isXHTML === 'undefined') {
		if (typeof isXHTMLGlobal === 'undefined') {
			isXHTMLGlobal = axe.utils.isXHTML(node.ownerDocument);
		}
		isXHTML = isXHTMLGlobal
	}
	...
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this addressed @WilcoFiers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I added in Dylan's axe.utils.matchesSelector suggestion as well, but this follows the structure you suggested.

matches.properties = function matchesProperties (node, matcher) {
node = node.actualNode || node;
const out = matches.fromFunction(
propName => node[propName],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need an undefined handler here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. fromString will handle this.

Copy link
Contributor

@dylanb dylanb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments and ran out of time...

@@ -0,0 +1,51 @@
/* exported matches */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to document this function for consumers of axe-core?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point maybe. I've been thinking about potentially using them as rule selectors. But while they are commons? We didn't document our other commons, except through JSDocs (which I'll do), so I don't think that needs to be in this PR. Happy to raise an issue for it though so we know to do it later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have introduced a npm script npm run api-docs, which generates jsdoc's based API documentation. This can be accessed via http://localhost/doc/api.

See - https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#full-api-reference-for-developers

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO commons functions should all be documented, especially with the increasing team collaboration coming up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like Jey said, collaborators can generate docs for commons.

'bar',
['bar', 'baz'],
/bar/,
function (nodeName) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this function be changed to

function (attributeValue) {
  return attributeValue === 'bar'
}

In order to make the use case more clear?

}))
})

it('works with virtual nodes', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we test more conditions against virtual nodes? E.g. property matchers?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

describe('with a `condition` property', function () {
it('calls condition and uses its return value as a matcher', function () {
fixture.innerHTML = '<div>foo</div>'
assert.isTrue(fromDefinition(fixture.firstChild, {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we pass the virtualNode into here or the node? Are there scenarios where the virtualNode would be needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there are. Certainly not with the matchers that are in here now.

if (Array.isArray(definition)) {
return definition.some(definition => matches(node, definition));
}
if (typeof definition === 'string') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use matchesSelector for all things that can be turned into a CSS selector (e.g. attributes)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only attributes with strings can be turned into selectors. This seemed like a cleaner way to do it. If you don't mind, I'd prefer to get this in and open an issue to investigate that separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened issue #1279 to look into this.

/* global matches */
let isXHTMLGlobal
matches.nodeName = function matchNodeName (node, matcher, { isXHTML } = {}) {
node = node.actualNode || node;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use matchesSelector here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works only if the matcher is a string, but sure. I'll do that.

@WilcoFiers WilcoFiers dismissed jeeyyy’s stale review December 17, 2018 14:18

Please update your review

jeeyyy
jeeyyy previously requested changes Dec 17, 2018
Copy link
Contributor

@jeeyyy jeeyyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some comments. Also, please re-run prettier and commit again.

})
}

matches.condition = function (node, condition) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for separation of concerns, I would put this in a single file.


// Check that the property has all the expected values
return Object.keys(matcher).every(propName => {
return matches.fromString(getValue(propName), matcher[propName]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this addressed @WilcoFiers?

@@ -0,0 +1,51 @@
/* exported matches */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have introduced a npm script npm run api-docs, which generates jsdoc's based API documentation. This can be accessed via http://localhost/doc/api.

See - https://github.com/dequelabs/axe-core/blob/develop/doc/API.md#full-api-reference-for-developers

if (typeof isXHTMLGlobal === 'undefined') {
isXHTMLGlobal = axe.utils.isXHTML(node.ownerDocument);
}
if (typeof isXHTML === 'undefined') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this addressed @WilcoFiers?

}))
})

it('matches a definition with an `properties` property', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

editorial nitpick definition with a properties property.

it('returns true when all matching properties return true', function () {
fixture.innerHTML = '<input value="bar" aria-disabled="true" />'
assert.isTrue(fromDefinition(fixture.firstChild, {
nodeName: 'input',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Loving the api. Very nice.

})
})

describe('with void matchers', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the thinking behind allowing null or undefined in .fromFunction matcher?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably wasn't necessary. I'm going to remove it.

{
nodeName: 'img',
attributes: {
alt: isNotNull
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work with empty quotes? Or is it just existence of the attribute, irrespective of values?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This matches an element without alt. If you want to match an element with an empty alt, do attributes: { alt : '' }. Looking back at the spec, I think that's what it should have been. Good find.

Edit: OK so I looked into this lookupTable.evaluateRoleForElement is handling the empty alt scenario. It's a bit wonky. I do think we should change that, but I'd prefer to keep that out of this PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilcoFiers can you file a new issue for that?

@@ -0,0 +1,8 @@
/* global matches */
matches.attributes = function matchesAttributes (node, matcher) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where/how does this method get called?

* Example:
* ```js
* // Match a single nodeName:
* axe.utils.elementMatch(elm, 'div')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems outdated–should it read matches instead of elementMatch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right you are.

matches.fromDefinition = function matchFromDefinition (node, definition) {
node = node.actualNode || node;
if (Array.isArray(definition)) {
return definition.some(definition => matches(node, definition));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reuse of the word definition as a variable in .some makes the recursive call a bit difficult to follow. Can you refine it to describe more accurately what is being provided as an argument in the recursive call to matches?

if (!matchers.includes(matcherName)) {
throw new Error(`Unknown matcher type "${matcherName}"`)
}
const matchMethod = matches[matcherName]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment here about what is happening with matches? It's so overloaded it's hard to tell exactly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was the challenge writing this code. I'll add in comments.

})
}

matches.condition = function (node, condition) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It definitely needs some comments about usage. Similar to matches.attributes, it's difficult to follow how the lookupTable functions with this matches object...

* Check if the value from a function matches some condition
*
* Each key on the matcher object is passed to getValue, the returned value
* must than match the with the value of that matcher
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This text could be improved: Each key on the matcher object is passed to getValue, the returned value must match with the value of that matcher

It would also benefit from an example like the other methods.

return true;
}
if (matcherType !== 'object' || Array.isArray(matcher) || matcher instanceof RegExp) {
throw new Error('Expect elementMatch properties to be an object');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is elementMatch correct here?

@@ -0,0 +1,9 @@
/* global matches */
matches.properties = function matchesProperties (node, matcher) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc comments would be good.

@WilcoFiers WilcoFiers dismissed stale reviews from marcysutton and jeeyyy December 18, 2018 12:15

I've processed all your comments. Please look again.

jeeyyy
jeeyyy previously approved these changes Dec 19, 2018
Copy link
Contributor

@jeeyyy jeeyyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

{
nodeName: 'img',
attributes: {
alt: isNotNull
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@WilcoFiers can you file a new issue for that?

@@ -0,0 +1,51 @@
/* exported matches */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO commons functions should all be documented, especially with the increasing team collaboration coming up.

@WilcoFiers WilcoFiers merged commit 986c97a into develop Jan 3, 2019
@WilcoFiers WilcoFiers deleted the matches branch January 3, 2019 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants