diff --git a/packages/cli/package-lock.json b/packages/cli/package-lock.json index 4185d4e9..9fdacf39 100644 --- a/packages/cli/package-lock.json +++ b/packages/cli/package-lock.json @@ -9,7 +9,6 @@ "version": "4.5.2", "license": "MPL-2.0", "dependencies": { - "@axe-core/webdriverjs": "^4.5.2", "axe-core": "^4.6.1", "chromedriver": "^108.0.0", "colors": "^1.4.0", @@ -52,17 +51,6 @@ "node": ">=6.0.0" } }, - "node_modules/@axe-core/webdriverjs": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@axe-core/webdriverjs/-/webdriverjs-4.5.2.tgz", - "integrity": "sha512-7OvK9y6vn4VEGiwosyQd0O14YWaDCIvrVDlDG8bbUauFS6jncZin0rVmuwrPWt40SpHY/uxJypfZgPJcENHuPw==", - "dependencies": { - "axe-core": "^4.5.2" - }, - "peerDependencies": { - "selenium-webdriver": ">3.0.0-beta || >=2.53.1 || >4.0.0-alpha" - } - }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -3713,14 +3701,6 @@ "@jridgewell/trace-mapping": "^0.3.9" } }, - "@axe-core/webdriverjs": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@axe-core/webdriverjs/-/webdriverjs-4.5.2.tgz", - "integrity": "sha512-7OvK9y6vn4VEGiwosyQd0O14YWaDCIvrVDlDG8bbUauFS6jncZin0rVmuwrPWt40SpHY/uxJypfZgPJcENHuPw==", - "requires": { - "axe-core": "^4.5.2" - } - }, "@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", diff --git a/packages/playwright/package-lock.json b/packages/playwright/package-lock.json index b70a5d20..d4a6a431 100644 --- a/packages/playwright/package-lock.json +++ b/packages/playwright/package-lock.json @@ -17,7 +17,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", @@ -803,8 +803,7 @@ }, "node_modules/axe-test-fixtures": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, "license": "MPL-2.0" }, @@ -4291,10 +4290,9 @@ "integrity": "sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w==" }, "axe-test-fixtures": { - "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, - "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures" + "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures#v1" }, "balanced-match": { "version": "1.0.0", diff --git a/packages/playwright/package.json b/packages/playwright/package.json index febfd757..59f906a1 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -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", diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 00b9530d..c4c4b230 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -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'; @@ -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; @@ -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; } @@ -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; } diff --git a/packages/playwright/src/types.ts b/packages/playwright/src/types.ts index cf7990ce..f1606e9d 100644 --- a/packages/playwright/src/types.ts +++ b/packages/playwright/src/types.ts @@ -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 { diff --git a/packages/playwright/src/utils.ts b/packages/playwright/src/utils.ts index 19b9f3a7..a828901d 100644 --- a/packages/playwright/src/utils.ts +++ b/packages/playwright/src/utils.ts @@ -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'; /** @@ -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); diff --git a/packages/playwright/tests/axe-playwright.spec.ts b/packages/playwright/tests/axe-playwright.spec.ts index 5f191caa..026c3d32 100644 --- a/packages/playwright/tests/axe-playwright.spec.ts +++ b/packages/playwright/tests/axe-playwright.spec.ts @@ -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'; @@ -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 () => { @@ -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', () => { diff --git a/packages/puppeteer/package-lock.json b/packages/puppeteer/package-lock.json index 2a21c87a..778a0214 100644 --- a/packages/puppeteer/package-lock.json +++ b/packages/puppeteer/package-lock.json @@ -18,7 +18,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", @@ -987,8 +987,7 @@ }, "node_modules/axe-test-fixtures": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, "license": "MPL-2.0" }, @@ -4996,10 +4995,9 @@ "integrity": "sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w==" }, "axe-test-fixtures": { - "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, - "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures" + "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures#v1" }, "balanced-match": { "version": "1.0.2", diff --git a/packages/puppeteer/package.json b/packages/puppeteer/package.json index 83928677..1f4b4eb2 100644 --- a/packages/puppeteer/package.json +++ b/packages/puppeteer/package.json @@ -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", diff --git a/packages/puppeteer/src/axePuppeteer.ts b/packages/puppeteer/src/axePuppeteer.ts index 56b5bd0a..d6ce5f38 100644 --- a/packages/puppeteer/src/axePuppeteer.ts +++ b/packages/puppeteer/src/axePuppeteer.ts @@ -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, @@ -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[]; @@ -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; } @@ -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; } diff --git a/packages/puppeteer/src/utils.ts b/packages/puppeteer/src/utils.ts index 48919303..1e2f766b 100644 --- a/packages/puppeteer/src/utils.ts +++ b/packages/puppeteer/src/utils.ts @@ -26,8 +26,8 @@ export function arrayify(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 = { diff --git a/packages/puppeteer/test/axePuppeteer.test.ts b/packages/puppeteer/test/axePuppeteer.test.ts index fd253e04..8f8d2c80 100644 --- a/packages/puppeteer/test/axePuppeteer.test.ts +++ b/packages/puppeteer/test/axePuppeteer.test.ts @@ -375,6 +375,15 @@ describe('AxePuppeteer', function () { describe('context', () => { describe('with include and exclude', () => { + const flatPassesTargets = (results: Axe.AxeResults): string[] => { + return results.passes + .reduce((acc, pass) => { + return acc.concat(pass.nodes as any); + }, []) + .reduce((acc, node: any) => { + return acc.concat(node.target.flat(1)); + }, []); + }; it('passes both .include and .exclude', async () => { const axeSource = ` window.axe = { @@ -405,6 +414,74 @@ describe('AxePuppeteer', function () { await expectAsyncToNotThrow(() => axePip.analyze()); }); + + it('with labelled frame', async () => { + await page.goto(`${addr}/external/context-include-exclude.html`); + const results = await new AxePuppeteer(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: Axe.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 AxePuppeteer(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 AxePuppeteer(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 AxePuppeteer(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 AxePuppeteer(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']); + }); }); // See #58 diff --git a/packages/webdriverio/package-lock.json b/packages/webdriverio/package-lock.json index 20b8af82..7b35a606 100644 --- a/packages/webdriverio/package-lock.json +++ b/packages/webdriverio/package-lock.json @@ -21,7 +21,7 @@ "@types/node": "^18.8.3", "@types/test-listen": "^1.1.0", "@wdio/sync": "^7.25.1", - "axe-test-fixtures": "github:dequelabs/axe-test-fixtures", + "axe-test-fixtures": "github:dequelabs/axe-test-fixtures#v1", "chai": "^4.3.6", "chromedriver": "^108.0.0", "delay": "^5.0.0", @@ -1230,8 +1230,7 @@ }, "node_modules/axe-test-fixtures": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, "license": "MPL-2.0" }, @@ -6347,10 +6346,9 @@ "integrity": "sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w==" }, "axe-test-fixtures": { - "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, - "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures" + "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures#v1" }, "axios": { "version": "1.2.1", diff --git a/packages/webdriverio/package.json b/packages/webdriverio/package.json index d855806b..9c18cec9 100644 --- a/packages/webdriverio/package.json +++ b/packages/webdriverio/package.json @@ -48,7 +48,7 @@ "@types/node": "^18.8.3", "@types/test-listen": "^1.1.0", "@wdio/sync": "^7.25.1", - "axe-test-fixtures": "github:dequelabs/axe-test-fixtures", + "axe-test-fixtures": "github:dequelabs/axe-test-fixtures#v1", "chai": "^4.3.6", "chromedriver": "^108.0.0", "delay": "^5.0.0", diff --git a/packages/webdriverio/src/index.ts b/packages/webdriverio/src/index.ts index 8556383d..a59bd415 100644 --- a/packages/webdriverio/src/index.ts +++ b/packages/webdriverio/src/index.ts @@ -14,7 +14,13 @@ import { } from './utils'; import type { Browser } from 'webdriverio'; -import type { RunOptions, AxeResults, SerialContextObject } from 'axe-core'; +import type { + RunOptions, + AxeResults, + SerialContextObject, + SerialSelectorList, + SerialFrameSelector +} from 'axe-core'; import type { Options, CallbackFunction, @@ -27,8 +33,8 @@ import type { export default class AxeBuilder { private client: Browser<'async'>; private axeSource: string; - private includes: Selector[] = []; - private excludes: Selector[] = []; + private includes: SerialSelectorList = []; + private excludes: SerialSelectorList = []; private option: RunOptions = {}; private disableFrameSelectors: string[] = []; private legacyMode = false; @@ -73,8 +79,7 @@ export default class AxeBuilder { * Selector to include in analysis. * This may be called any number of times. */ - public include(selector: Selector): this { - selector = Array.isArray(selector) ? selector : [selector]; + public include(selector: SerialFrameSelector): this { this.includes.push(selector); return this; } @@ -83,8 +88,7 @@ export default class AxeBuilder { * Selector to exclude in analysis. * This may be called any number of times. */ - public exclude(selector: Selector): this { - selector = Array.isArray(selector) ? selector : [selector]; + public exclude(selector: SerialFrameSelector): this { this.excludes.push(selector); return this; } diff --git a/packages/webdriverio/src/test.ts b/packages/webdriverio/src/test.ts index c367cfe3..736ffb81 100644 --- a/packages/webdriverio/src/test.ts +++ b/packages/webdriverio/src/test.ts @@ -12,7 +12,7 @@ import delay from 'delay'; import AxeBuilder from '.'; import { logOrRethrowError } from './utils'; import { WdioBrowser } from './types'; -import type { AxeResults } from 'axe-core'; +import type { AxeResults, Result } from 'axe-core'; const connectToChromeDriver = (port: number): Promise => { let socket: net.Socket; @@ -985,7 +985,7 @@ describe('@axe-core/webdriverio', () => { return acc.concat(pass.nodes as any); }, []) .reduce((acc, node: any) => { - return acc.concat(node.target); + return acc.concat(node.target.flat(1)); }, []); }; @@ -1119,6 +1119,90 @@ describe('@axe-core/webdriverio', () => { assert.isFalse(flatPassesTargets(results).includes('#hazaar')); assert.isDefined(labelResult); }); + + it('with labelled frame', async () => { + await client.url(`${addr}/context-include-exclude.html`); + const results = await new AxeBuilder({ client }) + .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 client.url(`${addr}/shadow-dom.html`); + const results = await new AxeBuilder({ client }) + .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 client.url(`${addr}/shadow-dom.html`); + const results = await new AxeBuilder({ client }) + .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 client.url(`${addr}/shadow-dom.html`); + const results = await new AxeBuilder({ client }) + .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 client.url(`${addr}/shadow-frames.html`); + const { violations } = await new AxeBuilder({ client }) + .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('setLegacyMode', () => { diff --git a/packages/webdriverio/src/utils.ts b/packages/webdriverio/src/utils.ts index e18f4692..242a7678 100644 --- a/packages/webdriverio/src/utils.ts +++ b/packages/webdriverio/src/utils.ts @@ -6,6 +6,7 @@ import type { RunOptions, Spec, PartialResults, + SerialSelectorList, SerialContextObject } from 'axe-core'; import type { Selector, WdioBrowser } from './types'; @@ -33,8 +34,8 @@ export const isWebdriverClient = (client: WdioBrowser): boolean => { * Get running context */ export const normalizeContext = ( - includes: Selector[], - excludes: Selector[], + includes: SerialSelectorList, + excludes: SerialSelectorList, disabledFrameSelectors: string[] ): SerialContextObject => { const base: SerialContextObject = { diff --git a/packages/webdriverjs/package-lock.json b/packages/webdriverjs/package-lock.json index 56b6e106..4184b1ae 100644 --- a/packages/webdriverjs/package-lock.json +++ b/packages/webdriverjs/package-lock.json @@ -21,7 +21,7 @@ "@types/selenium-webdriver": "^4.1.5", "@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", "chaimocha": "^1.10.0", "chromedriver": "^108.0.0", @@ -1010,8 +1010,7 @@ }, "node_modules/axe-test-fixtures": { "version": "1.0.0", - "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "resolved": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, "license": "MPL-2.0" }, @@ -5742,10 +5741,9 @@ "integrity": "sha512-lCZN5XRuOnpG4bpMq8v0khrWtUOn+i8lZSb6wHZH56ZfbIEv6XwJV84AAueh9/zi7qPVJ/E4yz6fmsiyOmXR4w==" }, "axe-test-fixtures": { - "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#ddc6e711051f0f8b834b7c01f1f056b04cf39e70", - "integrity": "sha512-liswp82xySChQGfFC/Ar6oWvwMPs+/TVE9vAG+OOaMpgGdlHLLbono82CVyctWNx8vu8R8SgSSdKS8MgQan34g==", + "version": "git+ssh://git@github.com/dequelabs/axe-test-fixtures.git#464337d0fb0f3f051dbb8483ca30c559d91b1746", "dev": true, - "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures" + "from": "axe-test-fixtures@github:dequelabs/axe-test-fixtures#v1" }, "axios": { "version": "1.2.1", diff --git a/packages/webdriverjs/package.json b/packages/webdriverjs/package.json index 8631af54..6fb88750 100644 --- a/packages/webdriverjs/package.json +++ b/packages/webdriverjs/package.json @@ -66,7 +66,7 @@ "@types/selenium-webdriver": "^4.1.5", "@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", "chaimocha": "^1.10.0", "chromedriver": "^108.0.0", diff --git a/packages/webdriverjs/src/index.ts b/packages/webdriverjs/src/index.ts index a7ccf6e0..fdf544a5 100644 --- a/packages/webdriverjs/src/index.ts +++ b/packages/webdriverjs/src/index.ts @@ -1,6 +1,13 @@ import { WebDriver } from 'selenium-webdriver'; -import { RunOptions, Spec, AxeResults, SerialContextObject } from 'axe-core'; -import { source } from 'axe-core'; +import { + RunOptions, + Spec, + AxeResults, + SerialContextObject, + SerialSelectorList, + source, + SerialFrameSelector +} from 'axe-core'; import { CallbackFunction, BuilderOptions, Selector } from './types'; import { normalizeContext } from './utils/index'; import AxeInjector from './axe-injector'; @@ -16,8 +23,8 @@ import assert from 'assert'; class AxeBuilder { private driver: WebDriver; private axeSource: string; - private includes: Selector[]; - private excludes: Selector[]; + private includes: SerialSelectorList; + private excludes: SerialSelectorList; private option: RunOptions; private config: Spec | null; private builderOptions: BuilderOptions; @@ -44,8 +51,7 @@ class AxeBuilder { * Selector to include in analysis. * This may be called any number of times. */ - public include(selector: Selector): this { - selector = Array.isArray(selector) ? selector : [selector]; + public include(selector: SerialFrameSelector): this { this.includes.push(selector); return this; } @@ -54,8 +60,7 @@ class AxeBuilder { * Selector to exclude in analysis. * This may be called any number of times. */ - public exclude(selector: Selector): this { - selector = Array.isArray(selector) ? selector : [selector]; + public exclude(selector: SerialFrameSelector): this { this.excludes.push(selector); return this; } diff --git a/packages/webdriverjs/src/utils/index.ts b/packages/webdriverjs/src/utils/index.ts index 6eb1fee1..db51638f 100644 --- a/packages/webdriverjs/src/utils/index.ts +++ b/packages/webdriverjs/src/utils/index.ts @@ -1,12 +1,12 @@ -import type { SerialContextObject } from 'axe-core'; +import type { SerialContextObject, SerialSelectorList } from 'axe-core'; import { Selector } from '../types'; /** * Get running context */ export const normalizeContext = ( - include: Selector[], - exclude: Selector[] + include: SerialSelectorList, + exclude: SerialSelectorList ): SerialContextObject => { const base: SerialContextObject = { exclude: [] diff --git a/packages/webdriverjs/tests/axe-webdriverjs.spec.ts b/packages/webdriverjs/tests/axe-webdriverjs.spec.ts index 9f438a92..ebd316e4 100644 --- a/packages/webdriverjs/tests/axe-webdriverjs.spec.ts +++ b/packages/webdriverjs/tests/axe-webdriverjs.spec.ts @@ -1,5 +1,5 @@ import 'mocha'; -import { AxeResults, Spec } from 'axe-core'; +import { AxeResults, Result } from 'axe-core'; import { WebDriver } from 'selenium-webdriver'; import express from 'express'; import chromedriver from 'chromedriver'; @@ -501,7 +501,7 @@ describe('@axe-core/webdriverjs', () => { 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 () => { @@ -615,6 +615,74 @@ describe('@axe-core/webdriverjs', () => { assert.strictEqual(results.violations[0].id, 'image-alt'); assert.strictEqual(results.violations[1].id, 'region'); }); + + it('with labelled frame', async () => { + await driver.get(`${addr}/external/context-include-exclude.html`); + const results = await new AxeBuilder(driver) + .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 driver.get(`${addr}/external/shadow-dom.html`); + const results = await new AxeBuilder(driver) + .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 driver.get(`${addr}/external/shadow-dom.html`); + const results = await new AxeBuilder(driver) + .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 driver.get(`${addr}/external/shadow-dom.html`); + const results = await new AxeBuilder(driver) + .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 driver.get(`${addr}/external/shadow-frames.html`); + const { violations } = await new AxeBuilder(driver) + .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('callback()', () => { diff --git a/packages/webdriverjs/tests/example/package-lock.json b/packages/webdriverjs/tests/example/package-lock.json index 6bce4d81..ef76f46a 100644 --- a/packages/webdriverjs/tests/example/package-lock.json +++ b/packages/webdriverjs/tests/example/package-lock.json @@ -10172,6 +10172,11 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "axe-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.6.0.tgz", + "integrity": "sha512-L3ZNbXPTxMrl0+qTXAzn9FBRvk5XdO56K8CvcCKtlxv44Aw2w2NCclGuvCWxHPw1Riiq3ncP/sxFYj2nUqdoTw==" + }, "axios": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.1.tgz",