Skip to content

Commit

Permalink
feat(context): allow selecting shadow DOM nodes (#3798)
Browse files Browse the repository at this point in the history
* feat(context): allow scoping shadow DOM nodes

* Refactor Context code

* Code complete

* cleanup

* Fix tests

* Update type definition

* Write docs

* Apply suggestions from code review

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>

* Resolve comments

* Add type definition tests

* Apply suggestions from code review

Co-authored-by: Erik Larsen <enlarsen@users.noreply.github.com>
Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>

* Update context.md

* fix test

* Update type test

Co-authored-by: Steven Lambert <2433219+straker@users.noreply.github.com>
Co-authored-by: Erik Larsen <enlarsen@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 1, 2022
1 parent 5026d65 commit 9e1e31b
Show file tree
Hide file tree
Showing 22 changed files with 1,630 additions and 658 deletions.
56 changes: 41 additions & 15 deletions axe.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,48 @@ declare namespace axe {
| 'embedded'
| 'interactive';

// Array of length 2 or greater
type MultiArray<T> = [T, T, ...T[]];

// Selectors within a frame
type BaseSelector = string;
type CrossTreeSelector = BaseSelector | BaseSelector[];
type CrossFrameSelector = CrossTreeSelector[];

type ContextObject = {
include?: Node | BaseSelector | Array<Node | BaseSelector | BaseSelector[]>;
exclude?: Node | BaseSelector | Array<Node | BaseSelector | BaseSelector[]>;
};
type ShadowDomSelector = MultiArray<BaseSelector>;
type CrossTreeSelector = BaseSelector | ShadowDomSelector;
type LabelledShadowDomSelector = { fromShadowDom: ShadowDomSelector };

type SerialContextObject = {
include?: BaseSelector | Array<BaseSelector | BaseSelector[]>;
exclude?: BaseSelector | Array<BaseSelector | BaseSelector[]>;
};
// Cross-frame selectors
type FramesSelector = Array<CrossTreeSelector | LabelledShadowDomSelector>;
type UnlabelledFrameSelector = CrossTreeSelector[];
type LabelledFramesSelector = { fromFrames: MultiArray<FramesSelector[0]> };
/**
* @deprecated Use UnlabelledFrameSelector instead
*/
type CrossFrameSelector = UnlabelledFrameSelector;

type RunCallback = (error: Error, results: AxeResults) => void;
// Context options
type Selector =
| Node
| BaseSelector
| LabelledShadowDomSelector
| LabelledFramesSelector;
type SelectorList = Array<Selector | FramesSelector> | NodeList;
type ContextObject =
| {
include: Selector | SelectorList;
exclude?: Selector | SelectorList;
}
| {
exclude: Selector | SelectorList;
};
type ElementContext = Selector | SelectorList | ContextObject;

interface SerialContextObject {
include: UnlabelledFrameSelector[];
exclude: UnlabelledFrameSelector[];
}

type ElementContext = Node | NodeList | string | ContextObject;
type RunCallback = (error: Error, results: AxeResults) => void;

interface TestEngine {
name: string;
Expand Down Expand Up @@ -255,9 +280,9 @@ declare namespace axe {
interface SerialDqElement {
source: string;
nodeIndexes: number[];
selector: CrossFrameSelector;
selector: UnlabelledFrameSelector;
xpath: string[];
ancestry: CrossFrameSelector;
ancestry: UnlabelledFrameSelector;
}
interface PartialRuleResult {
id: string;
Expand All @@ -273,7 +298,7 @@ declare namespace axe {
}
type PartialResults = Array<PartialResult | null>;
interface FrameContext {
frameSelector: CrossTreeSelector;
frameSelector: UnlabelledFrameSelector;
frameContext: SerialContextObject;
}
interface Utils {
Expand All @@ -282,6 +307,7 @@ declare namespace axe {
options?: RunOptions
) => FrameContext[];
shadowSelect: (selector: CrossTreeSelector) => Element | null;
shadowSelectAll: (selector: CrossTreeSelector) => Element[];
}
interface EnvironmentData {
testEngine: TestEngine;
Expand Down
76 changes: 17 additions & 59 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,106 +321,64 @@ 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")`
1. A NodeList such as returned by `document.querySelectorAll`.
1. A [CSS selector](./developer-guide.md#supported-css-selectors) that selects the portion(s) of the document that must be analyzed.
1. An include-exclude object (see below)
1. An object with `exclude` and/or `include` properties
1. An object with a `fromFrames` property
1. An object with a `fromShadowDom` property

###### Include-Exclude Object

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 Nodes or an array of arrays of [CSS selectors](./developer-guide.md#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
- All other's are the nested structure of iframes inside the document

In most cases, the component arrays will contain only one CSS selector. Multiple CSS selectors are only required if you want to include or exclude regions of a page that are inside iframes (or iframes within iframes within iframes). In this case, the first n-1 selectors are selectors that select the iframe(s) and the nth selector, selects the region(s) within the iframe.
Read [context.md](context.md) for details about the context object.

###### Context Parameter Examples

1. Include the first item in the `$fixture` NodeList but exclude its first child

```js
axe.run(
{
include: $fixture[0],
exclude: $fixture[0].firstChild
},
(err, results) => {
// ...
}
);
```

2. Include the element with the ID of `fix` but exclude any `div`s within it
1. Test the `#navBar` and all other `nav` elements and its content.

```js
axe.run(
{
include: [['#fix']],
exclude: [['#fix div']]
},
(err, results) => {
// ...
}
);
axe.run([`#navBar`, `nav`], (err, results) => {
// ...
});
```

3. Include the whole document except any structures whose parent contains the class `exclude1` or `exclude2`
2. Test everything except `.ad-banner` elements.

```js
axe.run(
{
exclude: [['.exclude1'], ['.exclude2']]
exclude: '.ad-banner'
},
(err, results) => {
// ...
}
);
```

4. Include the element with the ID of `fix`, within the iframe with id `frame`
3. Test the `form` element inside the `#payment` iframe.

```js
axe.run(
{
include: [['#frame', '#fix']]
fromFrames: ['iframe#payment', 'form']
},
(err, results) => {
// ...
}
);
```

5. Include the element with the ID of `fix`, within the iframe with id `frame2`, within the iframe with id `frame1`
4. Exclude all `.commentBody` elements in each `.commentsShadowHost` shadow DOM tree.

```js
axe.run(
{
include: [['#frame1', '#frame2', '#fix']]
exclude: {
fromShadowDom: ['.commentsShadowHost', '.commentBody']
}
},
(err, results) => {
// ...
}
);
```

6. Include the following:

- The element with the ID of `fix`, within the iframe with id `frame2`, within the iframe with id `frame1`
- The element with id `header`
- All links

```js
axe.run(
{
include: [['#header'], ['a'], ['#frame1', '#frame2', '#fix']]
},
(err, results) => {
// ...
}
);
```
More details on how to use the context object are described in [context.md](context.md).

##### Options Parameter

Expand Down
Loading

0 comments on commit 9e1e31b

Please sign in to comment.