Skip to content

Commit

Permalink
feat: support shadow dom selectors (#619)
Browse files Browse the repository at this point in the history
Co-authored-by: Steven Lambert <[email protected]>
  • Loading branch information
michael-siek and straker authored Dec 15, 2022
1 parent 8bab5b2 commit 6065b56
Show file tree
Hide file tree
Showing 23 changed files with 399 additions and 97 deletions.
20 changes: 0 additions & 20 deletions packages/cli/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 4 additions & 6 deletions packages/playwright/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/playwright/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@types/mocha": "^10.0.0",
"@types/node": "^18.8.3",
"@types/test-listen": "^1.1.0",
"axe-test-fixtures": "github:dequelabs/axe-test-fixtures",
"axe-test-fixtures": "github:dequelabs/axe-test-fixtures#v1",
"chai": "^4.3.6",
"express": "^4.18.2",
"mocha": "^10.0.0",
Expand Down
16 changes: 9 additions & 7 deletions packages/playwright/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import type {
RunOptions,
AxeResults,
SerialContextObject,
PartialResults
PartialResults,
ContextObject,
SerialSelectorList,
SerialSelector,
SerialFrameSelector
} from 'axe-core';
import { source } from 'axe-core';
import { normalizeContext, analyzePage } from './utils';
Expand All @@ -19,8 +23,8 @@ import AxePartialRunner from './AxePartialRunner';

export default class AxeBuilder {
private page: Page;
private includes: string[][];
private excludes: string[][];
private includes: SerialSelectorList;
private excludes: SerialSelectorList;
private option: RunOptions;
private source: string;
private legacyMode = false;
Expand All @@ -43,8 +47,7 @@ export default class AxeBuilder {
* @returns this
*/

public include(selector: string | string[]): this {
selector = Array.isArray(selector) ? selector : [selector];
public include(selector: SerialFrameSelector): this {
this.includes.push(selector);
return this;
}
Expand All @@ -56,8 +59,7 @@ export default class AxeBuilder {
* @returns this
*/

public exclude(selector: string | string[]): this {
selector = Array.isArray(selector) ? selector : [selector];
public exclude(selector: SerialFrameSelector): this {
this.excludes.push(selector);
return this;
}
Expand Down
7 changes: 4 additions & 3 deletions packages/playwright/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import type {
AxeResults,
ContextObject,
CrossTreeSelector,
PartialResult
PartialResult,
SerialContextObject
} from 'axe-core';

export type PartialResults = PartialResult | null;

export interface AnalyzePageParams {
context: ContextObject;
options: RunOptions | null;
context: SerialContextObject;
options: RunOptions;
}

export interface AxePlaywrightParams {
Expand Down
13 changes: 9 additions & 4 deletions packages/playwright/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import type { AxeResults, SerialContextObject } from 'axe-core';
import type {
AxeResults,
SerialContextObject,
SerialSelectorList
} from 'axe-core';
import type { AnalyzePageParams, AnalyzePageResponse } from './types';

/**
Expand All @@ -9,11 +13,12 @@ import type { AnalyzePageParams, AnalyzePageResponse } from './types';
*/

export const normalizeContext = (
includes: string[][],
excludes: string[][]
includes: SerialSelectorList,
excludes: SerialSelectorList
): SerialContextObject => {
const base: SerialContextObject = {
exclude: []
exclude: [],
include: []
};
if (excludes.length && Array.isArray(base.exclude)) {
base.exclude.push(...excludes);
Expand Down
77 changes: 75 additions & 2 deletions packages/playwright/tests/axe-playwright.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import 'mocha';
import fs from 'fs';
import playwright from 'playwright';
import express from 'express';
import type { AxeResults } from 'axe-core';
import type {
AxeResults,
Result,
SerialSelector,
SerialSelectorList
} from 'axe-core';
import testListen from 'test-listen';
import { assert } from 'chai';
import path from 'path';
Expand Down Expand Up @@ -426,7 +431,7 @@ describe('@axe-core/playwright', () => {
return acc.concat(pass.nodes as any);
}, [])
.reduce((acc, node: any) => {
return acc.concat(node.target);
return acc.concat(node.target.flat(1));
}, []);
};
it('with include and exclude', async () => {
Expand Down Expand Up @@ -557,6 +562,74 @@ describe('@axe-core/playwright', () => {
assert.equal(res?.status(), 200);
assert.deepEqual(actual[0], expected);
});

it('with labelled frame', async () => {
await page.goto(`${addr}/external/context-include-exclude.html`);
const results = await new AxeBuilder({ page })
.include({ fromFrames: ['#ifr-inc-excl', 'html'] })
.exclude({ fromFrames: ['#ifr-inc-excl', '#foo-bar'] })
.include({ fromFrames: ['#ifr-inc-excl', '#foo-baz', 'html'] })
.exclude({ fromFrames: ['#ifr-inc-excl', '#foo-baz', 'input'] })
.analyze();
const labelResult = results.violations.find(
(r: Result) => r.id === 'label'
);
assert.isFalse(flatPassesTargets(results).includes('#foo-bar'));
assert.isFalse(flatPassesTargets(results).includes('input'));
assert.isUndefined(labelResult);
});

it('with include shadow DOM', async () => {
await page.goto(`${addr}/external/shadow-dom.html`);
const results = await new AxeBuilder({ page })
.include([['#shadow-root-1', '#shadow-button-1']])
.include([['#shadow-root-2', '#shadow-button-2']])
.analyze();
assert.isTrue(flatPassesTargets(results).includes('#shadow-button-1'));
assert.isTrue(flatPassesTargets(results).includes('#shadow-button-2'));
assert.isFalse(flatPassesTargets(results).includes('#button'));
});

it('with exclude shadow DOM', async () => {
await page.goto(`${addr}/external/shadow-dom.html`);
const results = await new AxeBuilder({ page })
.exclude([['#shadow-root-1', '#shadow-button-1']])
.exclude([['#shadow-root-2', '#shadow-button-2']])
.analyze();
assert.isFalse(flatPassesTargets(results).includes('#shadow-button-1'));
assert.isFalse(flatPassesTargets(results).includes('#shadow-button-2'));
assert.isTrue(flatPassesTargets(results).includes('#button'));
});

it('with labelled shadow DOM', async () => {
await page.goto(`${addr}/external/shadow-dom.html`);
const results = await new AxeBuilder({ page })
.include({ fromShadowDom: ['#shadow-root-1', '#shadow-button-1'] })
.exclude({ fromShadowDom: ['#shadow-root-2', '#shadow-button-2'] })
.analyze();
assert.isTrue(flatPassesTargets(results).includes('#shadow-button-1'));
assert.isFalse(flatPassesTargets(results).includes('#shadow-button-2'));
});

it('with labelled iframe and shadow DOM', async () => {
await page.goto(`${addr}/external/shadow-frames.html`);
const { violations } = await new AxeBuilder({ page })
.exclude({
fromFrames: [
{
fromShadowDom: ['#shadow-root', '#shadow-frame']
},
'input'
]
})
.options({ runOnly: 'label' })
.analyze();
assert.equal(violations[0].id, 'label');
assert.lengthOf(violations[0].nodes, 2);
const nodes = violations[0].nodes;
assert.deepEqual(nodes[0].target, ['#light-frame', 'input']);
assert.deepEqual(nodes[1].target, ['#slotted-frame', 'input']);
});
});

describe('axe.finishRun errors', () => {
Expand Down
10 changes: 4 additions & 6 deletions packages/puppeteer/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/puppeteer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@types/node": "^18.8.3",
"@types/sinon": "^10.0.13",
"@types/test-listen": "^1.1.0",
"axe-test-fixtures": "github:dequelabs/axe-test-fixtures",
"axe-test-fixtures": "github:dequelabs/axe-test-fixtures#v1",
"chai": "^4.3.6",
"express": "^4.18.2",
"mocha": "^10.0.0",
Expand Down
19 changes: 12 additions & 7 deletions packages/puppeteer/src/axePuppeteer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import assert from 'assert';
import { RunOptions, SerialContextObject, Spec, AxeResults } from 'axe-core';
import {
RunOptions,
SerialContextObject,
Spec,
AxeResults,
SerialSelectorList,
SerialFrameSelector
} from 'axe-core';
import { Frame, Page } from 'puppeteer';
import {
axeGetFrameContext,
Expand All @@ -22,8 +29,8 @@ import {
export class AxePuppeteer {
private frame: Frame;
private axeSource?: string;
private includes: string[][];
private excludes: string[][];
private includes: SerialSelectorList;
private excludes: SerialSelectorList;
private axeOptions: RunOptions;
private config: Spec | null;
private disabledFrameSelectors: string[];
Expand Down Expand Up @@ -62,8 +69,7 @@ export class AxePuppeteer {
* Selector to include in analysis.
* This may be called any number of times.
*/
public include(selector: string | string[]): this {
selector = arrayify(selector);
public include(selector: SerialFrameSelector): this {
this.includes.push(selector);
return this;
}
Expand All @@ -72,8 +78,7 @@ export class AxePuppeteer {
* Selector to exclude in analysis.
* This may be called any number of times.
*/
public exclude(selector: string | string[]): this {
selector = arrayify(selector);
public exclude(selector: SerialFrameSelector): this {
this.excludes.push(selector);
return this;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/puppeteer/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export function arrayify<T>(src: T | T[]): T[] {
}

export function normalizeContext(
includes: string[][],
excludes: string[][],
includes: Axe.SerialSelectorList,
excludes: Axe.SerialSelectorList,
disabledFrameSelectors: string[]
): Axe.SerialContextObject {
const base: Axe.SerialContextObject = {
Expand Down
Loading

0 comments on commit 6065b56

Please sign in to comment.