Skip to content

Commit a9f9ee5

Browse files
strakerWilcoFiers
authored andcommitted
feat(utils): add support for complex CSS selectors (#1494)
Updates the CSS Selector Parser to allow more complex selectors: `:not`, `^=`, `$=`, and `*=`. Also update docs to describe what selectors we support. Closes: #1493 ## Reviewer checks **Required fields, to be filled out by PR reviewer(s)** - [x] Follows the commit message policy, appropriate for next version - [x] Has documentation updated, a DU ticket, or requires no documentation change - [x] Includes new tests, or was unnecessary - [x] Code is reviewed for security by: @WilcoFiers
1 parent 80a96cf commit a9f9ee5

File tree

5 files changed

+48
-11
lines changed

5 files changed

+48
-11
lines changed

doc/API.md

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -176,15 +176,15 @@ axe.configure({
176176
- The rules attribute is an Array of rule objects
177177
- each rule object can contain the following attributes
178178
- `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.
179-
- `selector` - string(optional, default `*`). A CSS selector used to identify the elements that are passed into the rule for evaluation.
179+
- `selector` - string(optional, default `*`). A [CSS selector](./developer-guide.md#supported-css-selectors) used to identify the elements that are passed into the rule for evaluation.
180180
- `excludeHidden` - boolean(optional, default `true`). This indicates whether elements that are hidden from all users are to be passed into the rule for evaluation.
181181
- `enabled` - boolean(optional, default `true`). Whether the rule is turned on. This is a common attribute for overriding.
182182
- `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).
183183
- `any` - array(optional, default `[]`). This is a list of checks that, if none "pass", will generate a violation.
184184
- `all` - array(optional, default `[]`). This is a list of checks that, if any "fails", will generate a violation.
185185
- `none` - array(optional, default `[]`). This is a list of checks that, if any "pass", will generate a violation.
186186
- `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"]
187-
- `matches` - string(optional, default `*`). A filtering CSS selector that will exclude elements that do not match the CSS selector.
187+
- `matches` - string(optional, default `*`). A filtering [CSS selector](./developer-guide.md#supported-css-selectors) that will exclude elements that do not match the CSS selector.
188188
- `disableOtherRules` - Disables all rules not included in the `rules` property.
189189
- `locale` - A locale object to apply (at runtime) to all rules and checks, in the same shape as `/locales/*.json`.
190190

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

253253
2. A NodeList such as returned by `document.querySelectorAll`.
254-
3. A CSS selector that selects the portion(s) of the document that must be analyzed. This includes:
255-
256-
- A CSS selector as a class name (e.g. `.classname`)
257-
- A CSS selector as a node name (e.g. `div`)
258-
- A CSS selector of an element id (e.g. `#tag`)
254+
3. A [CSS selector](./developer-guide.md#supported-css-selectors) that selects the portion(s) of the document that must be analyzed.
259255

260256
4. An include-exclude object (see below)
261257

@@ -264,7 +260,7 @@ By default, `axe.run` will test the entire document. The context object is an op
264260
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`.
265261

266262
- A node, or
267-
- An array of arrays of CSS selectors
263+
- An array of arrays of [CSS selectors](./developer-guide.md#supported-css-selectors)
268264
- If the nested array contains a single string, that string is the CSS selector
269265
- If the nested array contains multiple strings
270266
- The last string is the final CSS selector
@@ -703,7 +699,7 @@ axe.utils.querySelectorAll(virtualNode, 'a[href]');
703699
##### Parameters
704700

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

708704
##### Returns
709705

doc/developer-guide.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ Axe runs a series of tests to check for accessibility of content and functionali
55
Axe 3.0 supports open Shadow DOM: see our virtual DOM APIs and test utilities for developing axe-core moving forward. Note: we do not and cannot support closed Shadow DOM.
66

77
1. [Getting Started](#getting-started)
8+
1. [Environment Pre-requisites](#environment-pre-requisites)
9+
1. [Building axe.js](#building-axejs)
10+
1. [Running Tests](#running-tests)
11+
1. [API Reference](#api-reference)
12+
1. [Supported CSS Selectors](#supported-css-selectors)
813
1. [Architecture Overview](#architecture-overview)
914
1. [Rules](#rules)
1015
1. [Checks](#checks)
@@ -49,6 +54,15 @@ You can also load tests in any supported browser, which is helpful for debugging
4954

5055
[See API exposed on axe](./API.md#section-2-api-reference)
5156

57+
### Supported CSS Selectors
58+
59+
Axe supports the following CSS selectors:
60+
61+
- Type, Class, ID, and Universal selectors. E.g `div.main, #main`
62+
- Pseudo selector `not`. E.g `th:not([scope])`
63+
- Descendant and Child combinators. E.g. `table td`, `ul > li`
64+
- Attribute selectors `=`, `^=`, `$=`, `*=`. E.g `a[href^="#"]`
65+
5266
## Architecture Overview
5367

5468
Axe tests for accessibility using objects called Rules. Each Rule tests for a high-level aspect of accessibility, such as color contrast, button labels, and alternate text for images. Each rule is made up of a series of Checks. Depending on the rule; all, some, or none of these checks must pass in order for the rule to pass.
@@ -62,7 +76,7 @@ After execution, a Check will return `true` or `false` depending on whether or n
6276
Rules are defined by JSON files in the [lib/rules directory](../lib/rules). The JSON object is used to seed the [Rule object](../lib/core/base/rule.js#L30). A valid Rule JSON consists of the following:
6377

6478
- `id` - `String` A unique name of the Rule.
65-
- `selector` - **optional** `String` which is a CSS selector that specifies the elements of the page on which the Rule runs. axe-core will look inside of the light DOM and _open_ [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) trees for elements matching the provided selector. If omitted, the rule will run against every node.
79+
- `selector` - **optional** `String` which is a [CSS selector](#supported-css-selectors) that specifies the elements of the page on which the Rule runs. axe-core will look inside of the light DOM and _open_ [Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Shadow_DOM) trees for elements matching the provided selector. If omitted, the rule will run against every node.
6680
- `excludeHidden` - **optional** `Boolean` Whether the rule should exclude hidden elements. Defaults to `true`.
6781
- `enabled` - **optional** `Boolean` Whether the rule is enabled by default. Defaults to `true`.
6882
- `pageLevel` - **optional** `Boolean` Whether the rule is page level. Page level rules will only run if given an entire `document` as context.

lib/core/utils/css-parser.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
(function(axe) {
22
var parser = new axe.imports.CssSelectorParser();
3+
parser.registerSelectorPseudos('not');
34
parser.registerNestingOperators('>');
5+
parser.registerAttrEqualityMods('^', '$', '*');
46
axe.utils.cssParser = parser;
57
})(axe);

lib/core/utils/qsa.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ function convertPseudos(pseudos) {
152152
var expressions;
153153

154154
if (p.name === 'not') {
155-
expressions = axe.utils.cssParser.parse(p.value);
155+
expressions = p.value;
156156
expressions = expressions.selectors
157157
? expressions.selectors
158158
: [expressions];

test/core/utils/qsa.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ function Vnode(nodeName, className, attributes, id) {
55
this.attributes = attributes || [];
66
this.className = className;
77
this.nodeType = 1;
8+
9+
this.attributes.push({
10+
key: 'id',
11+
value: typeof id !== 'undefined' ? id : null
12+
});
13+
this.attributes.push({
14+
key: 'class',
15+
value: typeof className !== 'undefined' ? className : null
16+
});
817
}
918

1019
Vnode.prototype.getAttribute = function(att) {
@@ -217,6 +226,10 @@ describe('axe.utils.querySelectorAllFilter', function() {
217226
var result = axe.utils.querySelectorAllFilter(dom, 'div:not(#thangy)');
218227
assert.equal(result.length, 3);
219228
});
229+
it('should find nodes using :not selector with attribute', function() {
230+
var result = axe.utils.querySelectorAllFilter(dom, 'div:not([id])');
231+
assert.equal(result.length, 2);
232+
});
220233
it('should find nodes hierarchically using :not selector', function() {
221234
var result = axe.utils.querySelectorAllFilter(dom, 'div:not(.first) li');
222235
assert.equal(result.length, 2);
@@ -235,6 +248,18 @@ describe('axe.utils.querySelectorAllFilter', function() {
235248
);
236249
assert.equal(result.length, 0);
237250
});
251+
it('should find nodes using ^= attribute selector', function() {
252+
var result = axe.utils.querySelectorAllFilter(dom, '[class^="sec"]');
253+
assert.equal(result.length, 1);
254+
});
255+
it('should find nodes using $= attribute selector', function() {
256+
var result = axe.utils.querySelectorAllFilter(dom, '[id$="ne"]');
257+
assert.equal(result.length, 3);
258+
});
259+
it('should find nodes using *= attribute selector', function() {
260+
var result = axe.utils.querySelectorAllFilter(dom, '[role*="t"]');
261+
assert.equal(result.length, 2);
262+
});
238263
it('should put it all together', function() {
239264
var result = axe.utils.querySelectorAllFilter(
240265
dom,

0 commit comments

Comments
 (0)