UNPKG

@axe-core/playwright

Version:

Provides a method to inject and analyze web pages using axe

400 lines (392 loc) 11.7 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { AxeBuilder: () => AxeBuilder, default: () => AxeBuilder }); module.exports = __toCommonJS(src_exports); var import_assert = __toESM(require("assert")); var import_axe_core = __toESM(require("axe-core")); // src/utils.ts var normalizeContext = (includes, excludes) => { const base = { exclude: [], include: [] }; if (excludes.length && Array.isArray(base.exclude)) { base.exclude.push(...excludes); } if (includes.length) { base.include = includes; } return base; }; var analyzePage = ({ context, options }) => { const axeCore = window.axe; return axeCore.run(context || document, options || {}).then((results) => { return { error: null, results }; }).catch((err) => { return { error: err.message, results: null }; }); }; // src/browser.ts var axeGetFrameContexts = ({ context }) => { return window.axe.utils.getFrameContexts(context); }; var axeShadowSelect = ({ frameSelector }) => { return window.axe.utils.shadowSelect(frameSelector); }; var axeRunPartial = ({ context, options }) => { return window.axe.runPartial(context, options); }; var axeFinishRun = ({ options }) => { return window.axe.finishRun(JSON.parse(window.partialResults), options); }; function chunkResultString(chunk) { if (!window.partialResults) { window.partialResults = ""; } window.partialResults += chunk; } // src/AxePartialRunner.ts var AxePartialRunner = class { constructor(partialPromise, initiator = false) { this.initiator = initiator; this.partialPromise = caught(partialPromise); } partialPromise; childRunners = []; addChildResults(childResultRunner) { this.childRunners.push(childResultRunner); } async getPartials() { try { const parentPartial = await this.partialPromise; const childPromises = this.childRunners.map((childRunner) => { return childRunner ? caught(childRunner.getPartials()) : [null]; }); const childPartials = (await Promise.all(childPromises)).flat(1); return [parentPartial, ...childPartials]; } catch (e) { if (this.initiator) { throw e; } return [null]; } } }; var caught = /* @__PURE__ */ ((f) => { return (p) => (p.catch(f), p); })(() => { }); // src/index.ts var { source } = import_axe_core.default; var AxeBuilder = class { page; includes; excludes; option; axeSource; legacyMode = false; errorUrl; constructor({ page, axeSource }) { this.page = page; this.includes = []; this.excludes = []; this.option = {}; this.axeSource = axeSource; this.errorUrl = "https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/error-handling.md"; } /** * Selector to include in analysis. * This may be called any number of times. * @param String selector * @returns this */ include(selector) { this.includes.push(selector); return this; } /** * Selector to exclude in analysis. * This may be called any number of times. * @param String selector * @returns this */ exclude(selector) { this.excludes.push(selector); return this; } /** * Set options to be passed into axe-core * @param RunOptions options * @returns AxeBuilder */ options(options) { this.option = options; return this; } /** * Limit analysis to only the specified rules. * Cannot be used with `AxeBuilder#withTags` * @param String|Array rules * @returns this */ withRules(rules) { rules = Array.isArray(rules) ? rules : [rules]; this.option = this.option || {}; this.option.runOnly = { type: "rule", values: rules }; return this; } /** * Limit analysis to only specified tags. * Cannot be used with `AxeBuilder#withRules` * @param String|Array tags * @returns this */ withTags(tags) { tags = Array.isArray(tags) ? tags : [tags]; this.option = this.option || {}; this.option.runOnly = { type: "tag", values: tags }; return this; } /** * Set the list of rules to skip when running an analysis. * @param String|Array rules * @returns this */ disableRules(rules) { rules = Array.isArray(rules) ? rules : [rules]; this.option = this.option || {}; this.option.rules = {}; for (const rule of rules) { this.option.rules[rule] = { enabled: false }; } return this; } /** * Use frameMessenger with <same_origin_only> * * This disables use of axe.runPartial() which is called in each frame, and * axe.finishRun() which is called in a blank page. This uses axe.run() instead, * but with the restriction that cross-origin frames will not be tested. */ setLegacyMode(legacyMode = true) { this.legacyMode = legacyMode; return this; } /** * Perform analysis and retrieve results. *Does not chain.* * @return Promise<Result | Error> */ async analyze() { const context = normalizeContext(this.includes, this.excludes); const { page } = this; await page.evaluate(this.script()); const runPartialDefined = await page.evaluate( 'typeof window.axe.runPartial === "function"' ); let results; if (!runPartialDefined || this.legacyMode) { results = await this.runLegacy(context); return results; } const partialResults = await this.runPartialRecursive( page.mainFrame(), context ); const partials = await partialResults.getPartials(); try { return await this.finishRun(partials); } catch (error) { throw new Error( `${error.message} Please check out ${this.errorUrl}` ); } } /** * Injects `axe-core` into all frames. * @param Page - playwright page object * @returns Promise<void> */ async inject(frames, shouldThrow) { for (const iframe of frames) { const race = new Promise((_, reject) => { setTimeout(() => { reject(new Error("Script Timeout")); }, 1e3); }); const evaluate = iframe.evaluate(this.script()); try { await Promise.race([evaluate, race]); await iframe.evaluate(await this.axeConfigure()); } catch (err) { if (shouldThrow) { throw err; } } } } /** * Get axe-core source and configurations * @returns String */ script() { return this.axeSource || source; } async runLegacy(context) { const frames = this.page.frames(); await this.inject(frames); const axeResults = await this.page.evaluate(analyzePage, { context, options: this.option }); if (axeResults.error) { throw new Error(axeResults.error); } return axeResults.results; } /** * Inject `axe-core` into each frame and run `axe.runPartial`. * Because we need to inject axe into all frames all at once * (to avoid any potential problems with the DOM becoming out-of-sync) * but also need to not process results for any child frames if the parent * frame throws an error (requirements of the data structure for `axe.finishRun`), * we have to return a deeply nested array of Promises and then flatten * the array once all Promises have finished, throwing out any nested Promises * if the parent Promise is not fulfilled. * @param frame - playwright frame object * @param context - axe-core context object * @returns Promise<AxePartialRunner> */ async runPartialRecursive(frame, context) { const frameContexts = await frame.evaluate(axeGetFrameContexts, { context }); const partialPromise = frame.evaluate(axeRunPartial, { context, options: this.option }); const initiator = frame === this.page.mainFrame(); const axePartialRunner = new AxePartialRunner(partialPromise, initiator); for (const { frameSelector, frameContext } of frameContexts) { let childResults = null; try { const iframeHandle = await frame.evaluateHandle(axeShadowSelect, { frameSelector }); const iframeElement = iframeHandle.asElement(); const childFrame = await iframeElement.contentFrame(); if (childFrame) { await this.inject([childFrame], true); childResults = await this.runPartialRecursive( childFrame, frameContext ); } } catch { } axePartialRunner.addChildResults(childResults); } return axePartialRunner; } async finishRun(partialResults) { const { page, option: options } = this; const context = page.context(); const blankPage = await context.newPage(); (0, import_assert.default)( blankPage, "Please make sure that you have popup blockers disabled." ); await blankPage.evaluate(this.script()); await blankPage.evaluate(await this.axeConfigure()); const sizeLimit = 6e7; const partialString = JSON.stringify(partialResults); async function chunkResults(result) { const chunk = result.substring(0, sizeLimit); await blankPage.evaluate(chunkResultString, chunk); if (result.length > sizeLimit) { return await chunkResults(result.substr(sizeLimit)); } } await chunkResults(partialString); return await blankPage.evaluate(axeFinishRun, { options }).finally(async () => { await blankPage.close(); }); } async axeConfigure() { const hasRunPartial = await this.page.evaluate( 'typeof window.axe?.runPartial === "function"' ); return ` ;axe.configure({ ${!this.legacyMode && !hasRunPartial ? 'allowedOrigins: ["<unsafe_all_origins>"],' : 'allowedOrigins: ["<same_origin>"],'} branding: { application: 'playwright' } }) `; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { AxeBuilder }); if (module.exports.default) { var ___default_export = module.exports.default; var ___export_entries = Object.entries(module.exports); module.exports = ___default_export; ___export_entries.forEach(([key, value]) => { if (module.exports[key]) { throw new Error(`Export "${key}" already exists on default export`); } module.exports[key] = value; }); }