Repository: dequelabs/agnostic-axe Branch: master Commit: 04d7648329f9 Files: 10 Total size: 14.8 KB Directory structure: gitextract_jig8kg1k/ ├── .gitignore ├── LICENSE ├── README.MD ├── WEBPACK_EXAMPLE.MD ├── __tests__/ │ └── index.mjs ├── package.json └── src/ ├── AuditQueue.mjs ├── AxeObserver.mjs ├── index.mjs └── logViolations.mjs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # distribution dist # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # TypeScript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # next.js build output .next ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 LF Juliette Pretot Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.MD ================================================ # agnostic-axe Developer tool that continously observes the DOM to detect accessibility issues. Its audits are powered by [axe-core](https://github.com/dequelabs/axe-core). ![Screenshot of an opened website, with accessibility issues displayed in the browser console](screenshot.jpg) ## Basic Usage This is all you need to start reporting accessibility issues to the browser console: ```js import('https://unpkg.com/agnostic-axe@3').then( ({ AxeObserver, logViolations }) => { const MyAxeObserver = new AxeObserver(logViolations) MyAxeObserver.observe(document) } ) ``` > To try agnostic-axe, paste the above code into the browser console on a site of your choosing. When adding agnostic-axe to your project, be sure to only import it in your development environment. Else your application will use more resources than necessary. ([Here's an example of how to do this with webpack](WEBPACK_EXAMPLE.MD)) ## API Details ### AxeObserver constructor Accepts one parameter: - `violationsCallback` (required). A function that is invoked with an array of violations, as reported by [axe-core](https://github.com/dequelabs/axe-core). To log violations to the console, simply pass the `logViolations` function exported by this module. ### AxeObserver.observe Accepts one parameter: - `targetNode` (required). A DOM node. AxeObserver audits this node, and continously monitors it for changes. If a change has been detected, AxeObserver audits the parts that have changed, and reports any new accessibility defects. To observe multiple nodes, one can call the `AxeObserver.observe` method multiple times. ```js MyAxeObserver.observe(document.getElementById('react-main')) MyAxeObserver.observe(document.getElementById('vue-header')) MyAxeObserver.observe(document.getElementById('page-footer')) ``` ### AxeObserver.disconnect Accepts no parameters. Invoke this method to stop observing the DOM. This also clears the cache of violations that were already reported. ```js MyAxeObserver.disconnect() ``` ### Interacting with the axe-core API The instance of axe-core used by agnostic-axe is exported by this module. Import it to interact with the [axe-core API](https://github.com/dequelabs/axe-core/blob/develop/doc/API.md). ```js import('https://unpkg.com/agnostic-axe@3').then( ({ axeCoreInstance, AxeObserver, logViolations }) => { axeCoreInstance.registerPlugin(myPlugin) // ... } ) ``` ## Comparison with react-axe Unlike framework specific implementations of [axe-core](https://github.com/dequelabs/axe-core), such as [react-axe](https://github.com/dequelabs/react-axe), agnostic-axe uses a [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) to listen for changes directly in the DOM. This has two advantages: 1. It works with all web frameworks, and with any of their versions. This is key, as for example, at the time of writing, [react-axe](https://github.com/dequelabs/react-axe) does not work with the newer React features (function components and fragments), while agnostic-axe does supports them. 2. It only runs audits if the actual DOM changes. This means it uses less resources than [react-axe](https://github.com/dequelabs/react-axe), which runs audits when components rerender, even if their output does not change. agnostic-axe is optimized for performance. Its audits are small chunks of work that run in the browser's idle periods. ================================================ FILE: WEBPACK_EXAMPLE.MD ================================================ # Webpack Example ```js if (process.env.NODE_ENV !== 'production') { // Import agnostic axe here. // Webpack will comment out this code in production. import('https://unpkg.com/agnostic-axe@3').then( ({ AxeObserver, logViolations }) => { const MyAxeObserver = new AxeObserver(logViolations) MyAxeObserver.observe(document) } ) } ``` ================================================ FILE: __tests__/index.mjs ================================================ import Jasmine from 'jasmine' import JasmineConsoleReporter from 'jasmine-console-reporter' import * as AgnosticAxe from '../src/index.mjs' const jasmine = new Jasmine() jasmine.env.clearReporters() jasmine.addReporter( new JasmineConsoleReporter({ colors: true, cleanStack: true, verbosity: 4, listStyle: 'indent', activity: false }) ) jasmine.env.describe('The AgnosticAxe module', () => { jasmine.env.it('should export a `logViolations` function', () => { expect(typeof AgnosticAxe.logViolations).toBe('function') }) jasmine.env.it('should export a `AxeObserver` constructor', () => { expect(typeof AgnosticAxe.AxeObserver).toBe('function') }) }) jasmine.execute() ================================================ FILE: package.json ================================================ { "name": "agnostic-axe", "version": "3.0.3", "description": "Framework agnostic accessibility auditing with axe-core", "main": "dist/index.js", "module": "dist/index.mjs", "unpkg": "dist/standalone.mjs", "source": "src/index.mjs", "scripts": { "build": "npm run build:dependencies && npm run build:standalone", "build:dependencies": "npx microbundle -f es,cjs", "build:standalone": "npx microbundle --external none -f es -o dist/standalone.js", "dev": "npx microbundle --watch", "prepublishOnly": "npm run build", "test": "node --experimental-modules ./__tests__/index.mjs" }, "repository": { "type": "git", "url": "git+https://laurajuliette@github.com/juliettepretot/agnostic-axe.git" }, "keywords": [ "axe", "accessibility", "axe-core", "audit", "reporter" ], "author": "LF Juliette Pretot", "license": "MIT", "bugs": { "url": "https://github.com/juliettepretot/agnostic-axe/issues" }, "homepage": "https://github.com/juliettepretot/agnostic-axe#readme", "devDependencies": { "clean-slate-lint": "^1.0.9", "jasmine": "^3.5.0", "jasmine-console-reporter": "^3.1.0", "microbundle": "^0.11.0" }, "husky": { "hooks": { "pre-commit": "./node_modules/.bin/clean-slate-lint", "pre-rewrite": "./node_modules/.bin/clean-slate-lint" } }, "dependencies": { "axe-core": "^3.5.1", "idlize": "^0.1.1" } } ================================================ FILE: src/AuditQueue.mjs ================================================ import { rIC as requestIdleCallback } from 'idlize/idle-callback-polyfills.mjs' export default class AuditQueue { constructor() { this._pendingAudits = new Map() this._isRunning = false this.run = this.run.bind(this) this._scheduleAudits = this._scheduleAudits.bind(this) } run(node, getAuditResult) { if (this._pendingAudits.has(node)) { // This node is already scheduled to be audited. return null } // Returns a promise that resolves when this node is audited. return new Promise((resolve, reject) => { const runAudit = async () => { try { const result = await getAuditResult() resolve(await result) } catch (error) { reject(error) } } this._pendingAudits.set(node, runAudit) if (!this._isRunning) this._scheduleAudits() }) } _scheduleAudits() { this._isRunning = true requestIdleCallback(async IdleDeadline => { const iterator = this._pendingAudits.entries() for (const [node, runAudit] of iterator) { // Only run one audit at a time, as axe-core does not allow for // concurrent runs. // Ref: https://github.com/dequelabs/axe-core/issues/1041 await runAudit() this._pendingAudits.delete(node) if (IdleDeadline.timeRemaining() === 0) { break } } if (this._pendingAudits.size > 0) { // If pending audits remain, schedule them for the next idle phase. this._scheduleAudits() } else { // The queue is empty, we're no longer running this._isRunning = false } }) } } ================================================ FILE: src/AxeObserver.mjs ================================================ import axeCore from 'axe-core' import AuditQueue from './AuditQueue.mjs' // Apply default axeCore config axeCore.configure({ reporter: 'v2', checks: [ { id: 'color-contrast', options: { // Prevent axe from automatically scrolling noScroll: true } } ] }) export const axeCoreInstance = axeCore // Axe core does not allow parallel audits in the same env. Hence a queue is shared // across AxeObserver instances. const SharedAuditQueue = new AuditQueue() // The AxeObserver class takes a violationsCallback, which is invoked with an // array of observed violations. export default class AxeObserver { constructor(violationsCallback) { if (typeof violationsCallback !== 'function') { throw new Error( 'The AxeObserver constructor requires a violationsCallback' ) } this._violationsCallback = violationsCallback this.observe = this.observe.bind(this) this.disconnect = this.disconnect.bind(this) this._alreadyReportedIncidents = new Set() this._mutationObserver = new window.MutationObserver(mutationRecords => { mutationRecords.forEach(mutationRecord => { this._auditNode(mutationRecord.target) }) }) } observe(targetNode) { if (!targetNode) { throw new Error('AxeObserver.observe requires a targetNode') } this._mutationObserver.observe(targetNode, { attributes: true, subtree: true }) // run initial audit on the whole targetNode this._auditNode(targetNode) } disconnect() { this._mutationObserver.disconnect() this._alreadyReportedIncidents.clear() } async _auditNode(node) { const response = await SharedAuditQueue.run(node, async () => { // Since audits are scheduled asynchronously, it can happen that // the node is no longer connected. We cannot analyze it then. return node.isConnected ? axeCore.run(node) : null }) if (!response) return const violationsToReport = response.violations.filter(violation => { const filteredNodes = violation.nodes.filter(node => { const key = node.target.toString() + violation.id const wasAlreadyReported = this._alreadyReportedIncidents.has(key) if (wasAlreadyReported) { // filter out this violation for this node return false } else { // add to alreadyReportedIncidents as we'll report it now this._alreadyReportedIncidents.add(key) return true } }) return filteredNodes.length > 0 }) const hasViolationsToReport = violationsToReport.length > 0 if (hasViolationsToReport) { this._violationsCallback(violationsToReport) } } } ================================================ FILE: src/index.mjs ================================================ import _AxeObserver, { axeCoreInstance as _axeCoreInstance } from './AxeObserver.mjs' import _logViolations from './logViolations.mjs' export const AxeObserver = _AxeObserver export const axeCoreInstance = _axeCoreInstance export const logViolations = _logViolations ================================================ FILE: src/logViolations.mjs ================================================ import { axeCoreInstance } from './AxeObserver' const boldCourier = 'font-weight:bold;font-family:Courier;' const critical = 'color:red;font-weight:bold;' const serious = 'color:red;font-weight:normal;' const moderate = 'color:orange;font-weight:bold;' const minor = 'color:orange;font-weight:normal;' const defaultReset = 'font-color:black;font-weight:normal;' // The logViolations function takes an array of violations and logs them to the // console in a nice format. Its code is copied from the `react-axe` module. // Ref: https://github.com/dequelabs/react-axe export default function logViolations(violations) { if (violations.length) { console.group('%cNew aXe issues', serious) violations.forEach(function(result) { var fmt switch (result.impact) { case 'critical': fmt = critical break case 'serious': fmt = serious break case 'moderate': fmt = moderate break case 'minor': fmt = minor break default: fmt = minor break } console.groupCollapsed( '%c%s: %c%s %s', fmt, result.impact, defaultReset, result.help, result.helpUrl ) result.nodes.forEach(function(node) { failureSummary(node, 'any') failureSummary(node, 'none') }) console.groupEnd() }) console.groupEnd() } } function logElement(node, logFn) { var el = document.querySelector(node.target.toString()) if (!el) { logFn('Selector: %c%s', boldCourier, node.target.toString()) } else { logFn('Element: %o', el) } } function logHtml(node) { console.log('HTML: %c%s', boldCourier, node.html) } function logFailureMessage(node, key) { var message = axeCoreInstance._audit.data.failureSummaries[ key ].failureMessage( node[key].map(function(check) { return check.message || '' }) ) console.error(message) } function failureSummary(node, key) { if (node[key].length > 0) { logElement(node, console.groupCollapsed) logHtml(node) logFailureMessage(node, key) var relatedNodes = [] node[key].forEach(function(check) { relatedNodes = relatedNodes.concat(check.relatedNodes) }) if (relatedNodes.length > 0) { console.groupCollapsed('Related nodes') relatedNodes.forEach(function(relatedNode) { logElement(relatedNode, console.log) logHtml(relatedNode) }) console.groupEnd() } console.groupEnd() } }