[
  {
    "path": ".gitignore",
    "content": "# distribution\ndist\n\n# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n\n# next.js build output\n.next\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 LF Juliette Pretot\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.MD",
    "content": "# agnostic-axe\n\nDeveloper tool that continously observes the DOM to detect accessibility issues. Its audits are powered by [axe-core](https://github.com/dequelabs/axe-core).\n\n![Screenshot of an opened website, with accessibility issues displayed in the browser console](screenshot.jpg)\n\n## Basic Usage\n\nThis is all you need to start reporting accessibility issues to the browser console:\n\n```js\nimport('https://unpkg.com/agnostic-axe@3').then(\n  ({ AxeObserver, logViolations }) => {\n    const MyAxeObserver = new AxeObserver(logViolations)\n    MyAxeObserver.observe(document)\n  }\n)\n```\n\n> To try agnostic-axe, paste the above code into the browser console on a site of your choosing.\n\nWhen 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))\n\n## API Details\n\n### AxeObserver constructor\n\nAccepts one parameter:\n\n- `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.\n\n### AxeObserver.observe\n\nAccepts one parameter:\n\n- `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.\n\nTo observe multiple nodes, one can call the `AxeObserver.observe` method multiple times.\n\n```js\nMyAxeObserver.observe(document.getElementById('react-main'))\nMyAxeObserver.observe(document.getElementById('vue-header'))\nMyAxeObserver.observe(document.getElementById('page-footer'))\n```\n\n### AxeObserver.disconnect\n\nAccepts no parameters.\n\nInvoke this method to stop observing the DOM. This also clears the cache of violations that were already reported.\n\n```js\nMyAxeObserver.disconnect()\n```\n\n### Interacting with the axe-core API\n\nThe 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).\n\n```js\nimport('https://unpkg.com/agnostic-axe@3').then(\n  ({ axeCoreInstance, AxeObserver, logViolations }) => {\n    axeCoreInstance.registerPlugin(myPlugin)\n    // ...\n  }\n)\n```\n\n## Comparison with react-axe\n\nUnlike 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:\n\n1. 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.\n2. 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.\n\nagnostic-axe is optimized for performance. Its audits are small chunks of work that run in the browser's idle periods.\n"
  },
  {
    "path": "WEBPACK_EXAMPLE.MD",
    "content": "# Webpack Example\n\n```js\nif (process.env.NODE_ENV !== 'production') {\n  // Import agnostic axe here.\n  // Webpack will comment out this code in production.\n  import('https://unpkg.com/agnostic-axe@3').then(\n    ({ AxeObserver, logViolations }) => {\n      const MyAxeObserver = new AxeObserver(logViolations)\n      MyAxeObserver.observe(document)\n    }\n  )\n}\n```\n"
  },
  {
    "path": "__tests__/index.mjs",
    "content": "import Jasmine from 'jasmine'\nimport JasmineConsoleReporter from 'jasmine-console-reporter'\nimport * as AgnosticAxe from '../src/index.mjs'\n\nconst jasmine = new Jasmine()\n\njasmine.env.clearReporters()\njasmine.addReporter(\n  new JasmineConsoleReporter({\n    colors: true,\n    cleanStack: true,\n    verbosity: 4,\n    listStyle: 'indent',\n    activity: false\n  })\n)\n\njasmine.env.describe('The AgnosticAxe module', () => {\n  jasmine.env.it('should export a `logViolations` function', () => {\n    expect(typeof AgnosticAxe.logViolations).toBe('function')\n  })\n\n  jasmine.env.it('should export a `AxeObserver` constructor', () => {\n    expect(typeof AgnosticAxe.AxeObserver).toBe('function')\n  })\n})\n\njasmine.execute()\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"agnostic-axe\",\n  \"version\": \"3.0.3\",\n  \"description\": \"Framework agnostic accessibility auditing with axe-core\",\n  \"main\": \"dist/index.js\",\n  \"module\": \"dist/index.mjs\",\n  \"unpkg\": \"dist/standalone.mjs\",\n  \"source\": \"src/index.mjs\",\n  \"scripts\": {\n    \"build\": \"npm run build:dependencies && npm run build:standalone\",\n    \"build:dependencies\": \"npx microbundle -f es,cjs\",\n    \"build:standalone\": \"npx microbundle --external none -f es -o dist/standalone.js\",\n    \"dev\": \"npx microbundle --watch\",\n    \"prepublishOnly\": \"npm run build\",\n    \"test\": \"node --experimental-modules ./__tests__/index.mjs\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://laurajuliette@github.com/juliettepretot/agnostic-axe.git\"\n  },\n  \"keywords\": [\n    \"axe\",\n    \"accessibility\",\n    \"axe-core\",\n    \"audit\",\n    \"reporter\"\n  ],\n  \"author\": \"LF Juliette Pretot\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/juliettepretot/agnostic-axe/issues\"\n  },\n  \"homepage\": \"https://github.com/juliettepretot/agnostic-axe#readme\",\n  \"devDependencies\": {\n    \"clean-slate-lint\": \"^1.0.9\",\n    \"jasmine\": \"^3.5.0\",\n    \"jasmine-console-reporter\": \"^3.1.0\",\n    \"microbundle\": \"^0.11.0\"\n  },\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"./node_modules/.bin/clean-slate-lint\",\n      \"pre-rewrite\": \"./node_modules/.bin/clean-slate-lint\"\n    }\n  },\n  \"dependencies\": {\n    \"axe-core\": \"^3.5.1\",\n    \"idlize\": \"^0.1.1\"\n  }\n}\n"
  },
  {
    "path": "src/AuditQueue.mjs",
    "content": "import { rIC as requestIdleCallback } from 'idlize/idle-callback-polyfills.mjs'\n\nexport default class AuditQueue {\n  constructor() {\n    this._pendingAudits = new Map()\n    this._isRunning = false\n\n    this.run = this.run.bind(this)\n    this._scheduleAudits = this._scheduleAudits.bind(this)\n  }\n  run(node, getAuditResult) {\n    if (this._pendingAudits.has(node)) {\n      // This node is already scheduled to be audited.\n      return null\n    }\n\n    // Returns a promise that resolves when this node is audited.\n    return new Promise((resolve, reject) => {\n      const runAudit = async () => {\n        try {\n          const result = await getAuditResult()\n          resolve(await result)\n        } catch (error) {\n          reject(error)\n        }\n      }\n\n      this._pendingAudits.set(node, runAudit)\n      if (!this._isRunning) this._scheduleAudits()\n    })\n  }\n  _scheduleAudits() {\n    this._isRunning = true\n    requestIdleCallback(async IdleDeadline => {\n      const iterator = this._pendingAudits.entries()\n\n      for (const [node, runAudit] of iterator) {\n        // Only run one audit at a time, as axe-core does not allow for\n        // concurrent runs.\n        // Ref: https://github.com/dequelabs/axe-core/issues/1041\n        await runAudit()\n        this._pendingAudits.delete(node)\n\n        if (IdleDeadline.timeRemaining() === 0) {\n          break\n        }\n      }\n\n      if (this._pendingAudits.size > 0) {\n        // If pending audits remain, schedule them for the next idle phase.\n        this._scheduleAudits()\n      } else {\n        // The queue is empty, we're no longer running\n        this._isRunning = false\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "src/AxeObserver.mjs",
    "content": "import axeCore from 'axe-core'\nimport AuditQueue from './AuditQueue.mjs'\n\n// Apply default axeCore config\naxeCore.configure({\n  reporter: 'v2',\n  checks: [\n    {\n      id: 'color-contrast',\n      options: {\n        // Prevent axe from automatically scrolling\n        noScroll: true\n      }\n    }\n  ]\n})\n\nexport const axeCoreInstance = axeCore\n\n// Axe core does not allow parallel audits in the same env. Hence a queue is shared\n// across AxeObserver instances.\nconst SharedAuditQueue = new AuditQueue()\n\n// The AxeObserver class takes a violationsCallback, which is invoked with an\n// array of observed violations.\nexport default class AxeObserver {\n  constructor(violationsCallback) {\n    if (typeof violationsCallback !== 'function') {\n      throw new Error(\n        'The AxeObserver constructor requires a violationsCallback'\n      )\n    }\n\n    this._violationsCallback = violationsCallback\n\n    this.observe = this.observe.bind(this)\n    this.disconnect = this.disconnect.bind(this)\n\n    this._alreadyReportedIncidents = new Set()\n    this._mutationObserver = new window.MutationObserver(mutationRecords => {\n      mutationRecords.forEach(mutationRecord => {\n        this._auditNode(mutationRecord.target)\n      })\n    })\n  }\n  observe(targetNode) {\n    if (!targetNode) {\n      throw new Error('AxeObserver.observe requires a targetNode')\n    }\n\n    this._mutationObserver.observe(targetNode, {\n      attributes: true,\n      subtree: true\n    })\n\n    // run initial audit on the whole targetNode\n    this._auditNode(targetNode)\n  }\n  disconnect() {\n    this._mutationObserver.disconnect()\n    this._alreadyReportedIncidents.clear()\n  }\n  async _auditNode(node) {\n    const response = await SharedAuditQueue.run(node, async () => {\n      // Since audits are scheduled asynchronously, it can happen that\n      // the node is no longer connected. We cannot analyze it then.\n      return node.isConnected ? axeCore.run(node) : null\n    })\n\n    if (!response) return\n\n    const violationsToReport = response.violations.filter(violation => {\n      const filteredNodes = violation.nodes.filter(node => {\n        const key = node.target.toString() + violation.id\n\n        const wasAlreadyReported = this._alreadyReportedIncidents.has(key)\n\n        if (wasAlreadyReported) {\n          // filter out this violation for this node\n          return false\n        } else {\n          // add to alreadyReportedIncidents as we'll report it now\n          this._alreadyReportedIncidents.add(key)\n          return true\n        }\n      })\n\n      return filteredNodes.length > 0\n    })\n\n    const hasViolationsToReport = violationsToReport.length > 0\n\n    if (hasViolationsToReport) {\n      this._violationsCallback(violationsToReport)\n    }\n  }\n}\n"
  },
  {
    "path": "src/index.mjs",
    "content": "import _AxeObserver, {\n  axeCoreInstance as _axeCoreInstance\n} from './AxeObserver.mjs'\nimport _logViolations from './logViolations.mjs'\n\nexport const AxeObserver = _AxeObserver\nexport const axeCoreInstance = _axeCoreInstance\nexport const logViolations = _logViolations\n"
  },
  {
    "path": "src/logViolations.mjs",
    "content": "import { axeCoreInstance } from './AxeObserver'\n\nconst boldCourier = 'font-weight:bold;font-family:Courier;'\nconst critical = 'color:red;font-weight:bold;'\nconst serious = 'color:red;font-weight:normal;'\nconst moderate = 'color:orange;font-weight:bold;'\nconst minor = 'color:orange;font-weight:normal;'\nconst defaultReset = 'font-color:black;font-weight:normal;'\n\n// The logViolations function takes an array of violations and logs them to the\n// console in a nice format. Its code is copied from the `react-axe` module.\n// Ref: https://github.com/dequelabs/react-axe\nexport default function logViolations(violations) {\n  if (violations.length) {\n    console.group('%cNew aXe issues', serious)\n    violations.forEach(function(result) {\n      var fmt\n      switch (result.impact) {\n        case 'critical':\n          fmt = critical\n          break\n        case 'serious':\n          fmt = serious\n          break\n        case 'moderate':\n          fmt = moderate\n          break\n        case 'minor':\n          fmt = minor\n          break\n        default:\n          fmt = minor\n          break\n      }\n      console.groupCollapsed(\n        '%c%s: %c%s %s',\n        fmt,\n        result.impact,\n        defaultReset,\n        result.help,\n        result.helpUrl\n      )\n      result.nodes.forEach(function(node) {\n        failureSummary(node, 'any')\n        failureSummary(node, 'none')\n      })\n      console.groupEnd()\n    })\n    console.groupEnd()\n  }\n}\n\nfunction logElement(node, logFn) {\n  var el = document.querySelector(node.target.toString())\n  if (!el) {\n    logFn('Selector: %c%s', boldCourier, node.target.toString())\n  } else {\n    logFn('Element: %o', el)\n  }\n}\n\nfunction logHtml(node) {\n  console.log('HTML: %c%s', boldCourier, node.html)\n}\n\nfunction logFailureMessage(node, key) {\n  var message = axeCoreInstance._audit.data.failureSummaries[\n    key\n  ].failureMessage(\n    node[key].map(function(check) {\n      return check.message || ''\n    })\n  )\n\n  console.error(message)\n}\n\nfunction failureSummary(node, key) {\n  if (node[key].length > 0) {\n    logElement(node, console.groupCollapsed)\n    logHtml(node)\n    logFailureMessage(node, key)\n\n    var relatedNodes = []\n    node[key].forEach(function(check) {\n      relatedNodes = relatedNodes.concat(check.relatedNodes)\n    })\n\n    if (relatedNodes.length > 0) {\n      console.groupCollapsed('Related nodes')\n      relatedNodes.forEach(function(relatedNode) {\n        logElement(relatedNode, console.log)\n        logHtml(relatedNode)\n      })\n      console.groupEnd()\n    }\n\n    console.groupEnd()\n  }\n}\n"
  }
]