-
Notifications
You must be signed in to change notification settings - Fork 792
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
Conversation
There was a problem hiding this 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.
lib/commons/aria/index.js
Outdated
function isNull (value) { | ||
return value === null | ||
} | ||
function notNull (value) { |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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. 🎆
lib/commons/matches/attributes.js
Outdated
@@ -0,0 +1,8 @@ | |||
/* global matches */ | |||
matches.attributes = function matchesAttributes (node, matcher) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jsdoc
comments.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
lib/commons/matches/from-function.js
Outdated
|
||
// Check that the property has all the expected values | ||
return Object.keys(matcher).every(propName => { | ||
return matches.fromString(getValue(propName), matcher[propName]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this addressed @WilcoFiers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the only scenario where I expect difficulties.
'condition' | ||
] | ||
|
||
matches.fromDefinition = function matchFromDefinition (node, definition) { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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
}
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this addressed @WilcoFiers?
There was a problem hiding this comment.
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.
lib/commons/matches/properties.js
Outdated
matches.properties = function matchesProperties (node, matcher) { | ||
node = node.actualNode || node; | ||
const out = matches.fromFunction( | ||
propName => node[propName], |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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 */ |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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 () { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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') { |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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.
lib/commons/matches/from-function.js
Outdated
|
||
// Check that the property has all the expected values | ||
return Object.keys(matcher).every(propName => { | ||
return matches.fromString(getValue(propName), matcher[propName]); |
There was a problem hiding this comment.
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 */ |
There was a problem hiding this comment.
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
.
if (typeof isXHTMLGlobal === 'undefined') { | ||
isXHTMLGlobal = axe.utils.isXHTML(node.ownerDocument); | ||
} | ||
if (typeof isXHTML === 'undefined') { |
There was a problem hiding this comment.
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 () { |
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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 () { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
lib/commons/matches/attributes.js
Outdated
@@ -0,0 +1,8 @@ | |||
/* global matches */ | |||
matches.attributes = function matchesAttributes (node, matcher) { |
There was a problem hiding this comment.
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?
lib/commons/matches/index.js
Outdated
* Example: | ||
* ```js | ||
* // Match a single nodeName: | ||
* axe.utils.elementMatch(elm, 'div') |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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)); |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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...
lib/commons/matches/from-function.js
Outdated
* 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 |
There was a problem hiding this comment.
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.
lib/commons/matches/from-function.js
Outdated
return true; | ||
} | ||
if (matcherType !== 'object' || Array.isArray(matcher) || matcher instanceof RegExp) { | ||
throw new Error('Expect elementMatch properties to be an object'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is elementMatch
correct here?
lib/commons/matches/properties.js
Outdated
@@ -0,0 +1,9 @@ | |||
/* global matches */ | |||
matches.properties = function matchesProperties (node, matcher) { |
There was a problem hiding this comment.
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.
I've processed all your comments. Please look again.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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 */ |
There was a problem hiding this comment.
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.
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)