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(utils): add support for complex CSS selectors #1494

Merged
merged 5 commits into from
Apr 12, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
1. [Section 2: API Reference](#section-2-api-reference)
1. [Overview](#overview)
1. [API Notes](#api-notes)
1. [Supported CSS Selectors](#supported-css-selectors)
1. [API Name: axe.getRules](#api-name-axegetrules)
1. [API Name: axe.configure](#api-name-axeconfigure)
1. [API Name: axe.reset](#api-name-axereset)
Expand Down Expand Up @@ -65,6 +66,15 @@ For a full listing of API offered by aXe, clone the repository and run `npm run
- The `"helpUrl"` in the results object is a link to a broader description of the accessibility issue and suggested remediation. These links point to Deque University help pages, which do not require a login.
- aXe does not test hidden regions, such as inactive menus or modal windows. To test those for accessibility, write tests that activate or render the regions visible and run the analysis again.

#### Supported CSS Selectors
straker marked this conversation as resolved.
Show resolved Hide resolved

Axe supports the following CSS selectors:
Copy link
Member

Choose a reason for hiding this comment

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

Thanks.


- Type, Class, ID, and Universal selectors. E.g `div.main, #main`
- Pseudo selector `not`. E.g `th:not([scope])`
- Descendant and Child combinators. E.g. `table td`, `ul > li`
- Attribute selectors `=`, `^=`, `$=`, `*=`. E.g `a[href^="#"]`

### API Name: axe.getRules

#### Purpose
Expand Down Expand Up @@ -176,15 +186,15 @@ axe.configure({
- The rules attribute is an Array of rule objects
- each rule object can contain the following attributes
- `id` - string(required). This uniquely identifies the rule. If the rule already exists, it will be overridden with any of the attributes supplied. The attributes below that are marked required, are only required for new rules.
- `selector` - string(optional, default `*`). A CSS selector used to identify the elements that are passed into the rule for evaluation.
- `selector` - string(optional, default `*`). A [CSS selector](#supported-css-selectors) used to identify the elements that are passed into the rule for evaluation.
straker marked this conversation as resolved.
Show resolved Hide resolved
- `excludeHidden` - boolean(optional, default `true`). This indicates whether elements that are hidden from all users are to be passed into the rule for evaluation.
- `enabled` - boolean(optional, default `true`). Whether the rule is turned on. This is a common attribute for overriding.
- `pageLevel` - boolean(optional, default `false`). When set to true, this rule is only applied when the entire page is tested. Results from nodes on different frames are combined into a single result. See [page level rules](#page-level-rules).
- `any` - array(optional, default `[]`). This is a list of checks that, if none "pass", will generate a violation.
- `all` - array(optional, default `[]`). This is a list of checks that, if any "fails", will generate a violation.
- `none` - array(optional, default `[]`). This is a list of checks that, if any "pass", will generate a violation.
- `tags` - array(optional, default `[]`). A list if the tags that "classify" the rule. In practice, you must supply some valid tags or the default evaluation will not invoke the rule. The convention is to include the standard (WCAG 2 and/or section 508), the WCAG 2 level, Section 508 paragraph, and the WCAG 2 success criteria. Tags are constructed by converting all letters to lower case, removing spaces and periods and concatinating the result. E.g. WCAG 2 A success criteria 1.1.1 would become ["wcag2a", "wcag111"]
- `matches` - string(optional, default `*`). A filtering CSS selector that will exclude elements that do not match the CSS selector.
- `matches` - string(optional, default `*`). A filtering [CSS selector](#supported-css-selectors) that will exclude elements that do not match the CSS selector.
- `disableOtherRules` - Disables all rules not included in the `rules` property.
- `locale` - A locale object to apply (at runtime) to all rules and checks, in the same shape as `/locales/*.json`.

Expand Down Expand Up @@ -251,11 +261,7 @@ By default, `axe.run` will test the entire document. The context object is an op
- Example: To limit analysis to the `<div id="content">` element: `document.getElementById("content")`

2. A NodeList such as returned by `document.querySelectorAll`.
3. A CSS selector that selects the portion(s) of the document that must be analyzed. This includes:

- A CSS selector as a class name (e.g. `.classname`)
- A CSS selector as a node name (e.g. `div`)
- A CSS selector of an element id (e.g. `#tag`)
3. A [CSS selector](#supported-css-selectors) that selects the portion(s) of the document that must be analyzed.

4. An include-exclude object (see below)

Expand All @@ -264,7 +270,7 @@ By default, `axe.run` will test the entire document. The context object is an op
The include exclude object is a JSON object with two attributes: include and exclude. Either include or exclude is required. If only `exclude` is specified; include will default to the entire `document`.

- A node, or
- An array of arrays of CSS selectors
- An array of arrays of [CSS selectors](#supported-css-selectors)
- If the nested array contains a single string, that string is the CSS selector
- If the nested array contains multiple strings
- The last string is the final CSS selector
Expand Down Expand Up @@ -703,7 +709,7 @@ axe.utils.querySelectorAll(virtualNode, 'a[href]');
##### Parameters

- `virtualNode` – object, the flattened DOM tree to query against. `axe._tree` is available for this purpose during an audit; see below.
- `selector` – string, the CSS selector to use as a filter. For the most part, this should work seamlessly with `document.querySelectorAll`.
- `selector` – string, the [CSS selector](#supported-css-selectors) to use as a filter. For the most part, this should work seamlessly with `document.querySelectorAll`.

##### Returns

Expand Down
2 changes: 2 additions & 0 deletions lib/core/utils/css-parser.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
(function(axe) {
var parser = new axe.imports.CssSelectorParser();
parser.registerSelectorPseudos('not');
parser.registerNestingOperators('>');
parser.registerAttrEqualityMods('^', '$', '*');
axe.utils.cssParser = parser;
})(axe);
2 changes: 1 addition & 1 deletion lib/core/utils/qsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function convertPseudos(pseudos) {
var expressions;

if (p.name === 'not') {
expressions = axe.utils.cssParser.parse(p.value);
expressions = p.value;
expressions = expressions.selectors
? expressions.selectors
: [expressions];
Expand Down
25 changes: 25 additions & 0 deletions test/core/utils/qsa.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ function Vnode(nodeName, className, attributes, id) {
this.attributes = attributes || [];
this.className = className;
this.nodeType = 1;

this.attributes.push({
key: 'id',
value: typeof id !== 'undefined' ? id : null
});
this.attributes.push({
key: 'class',
value: typeof className !== 'undefined' ? className : null
});
}

Vnode.prototype.getAttribute = function(att) {
Expand Down Expand Up @@ -217,6 +226,10 @@ describe('axe.utils.querySelectorAllFilter', function() {
var result = axe.utils.querySelectorAllFilter(dom, 'div:not(#thangy)');
assert.equal(result.length, 3);
});
it('should find nodes using :not selector with attribute', function() {
var result = axe.utils.querySelectorAllFilter(dom, 'div:not([id])');
assert.equal(result.length, 2);
});
it('should find nodes hierarchically using :not selector', function() {
var result = axe.utils.querySelectorAllFilter(dom, 'div:not(.first) li');
assert.equal(result.length, 2);
Expand All @@ -235,6 +248,18 @@ describe('axe.utils.querySelectorAllFilter', function() {
);
assert.equal(result.length, 0);
});
it('should find nodes using ^= attribute selector', function() {
var result = axe.utils.querySelectorAllFilter(dom, '[class^="sec"]');
assert.equal(result.length, 1);
});
it('should find nodes using $= attribute selector', function() {
var result = axe.utils.querySelectorAllFilter(dom, '[id$="ne"]');
assert.equal(result.length, 3);
});
it('should find nodes using *= attribute selector', function() {
var result = axe.utils.querySelectorAllFilter(dom, '[role*="t"]');
assert.equal(result.length, 2);
});
it('should put it all together', function() {
var result = axe.utils.querySelectorAllFilter(
dom,
Expand Down