Skip to content

Commit

Permalink
feat: add @axe-core/react (#53)
Browse files Browse the repository at this point in the history
Co-authored-by: Stephen Mathieson <me@stephenmathieson.com>
Co-authored-by: Wilco Fiers <WilcoFiers@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 6, 2020
1 parent 2e37e65 commit ea3b15a
Show file tree
Hide file tree
Showing 30 changed files with 6,911 additions and 5 deletions.
32 changes: 29 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,17 @@ jobs:
- restore_cache:
keys:
- v1-reporter-earl-npm-{{ checksum "packages/reporter-earl/package-lock.json" }}
- restore_cache:
keys:
- v1-react-npm-{{ checksum "packages/react/package-lock.json" }}
- restore_cache:
keys:
- v1-axe-core-test-npm-{{ checksum "axe_core_test/package-lock.json" }}

- run: npm ci
- run: npm run bootstrap -- --ci
- save_cache:
key: v1-root-npm-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run: npm run bootstrap -- --ci
- save_cache:
key: v1-cli-npm-{{ checksum "packages/cli/package-lock.json" }}
paths:
Expand All @@ -72,6 +73,10 @@ jobs:
key: v1-reporter-earl-npm-{{ checksum "packages/reporter-earl/package-lock.json" }}
paths:
- packages/reporter-earl/node_modules
- save_cache:
key: v1-react-npm-{{ checksum "packages/react/package-lock.json" }}
paths:
- packages/react/node_modules
- save_cache:
key: v1-axe-core-test-npm-{{ checksum "axe_core_test/package-lock.json" }}
paths:
Expand All @@ -85,6 +90,8 @@ jobs:
- packages/webdriverjs/node_modules
- packages/webdriverio/node_modules
- packages/reporter-earl/node_modules
- packages/react/node_modules
- packages/react/example/node_modules
- axe_core_test/node_modules

lint:
Expand Down Expand Up @@ -124,13 +131,23 @@ jobs:
- checkout
- restore_dependency_cache
- run: npm run test --prefix=packages/webdriverio

reporter-earl:
<<: *defaults
steps:
- checkout
- restore_dependency_cache
- run: npm run test --prefix=packages/reporter-earl

react:
<<: *defaults
steps:
- checkout
- restore_dependency_cache
- run: npm install cypress --prefix=packages/react
- run: npm run build --prefix=packages/react
- run: npm run test --prefix=packages/react

axe-core-test:
<<: *defaults
steps:
Expand All @@ -147,6 +164,8 @@ jobs:
# Make sure the working tree is clean
- run: git reset --hard HEAD
- run: npx lerna publish prepatch --no-verify-access --no-git-tag-version --no-push --canary --dist-tag=next --exact --force-publish --yes
# Verify that @axe-core/react has all requires files
- run: ./axe-core-npm/packages/react/verify-release.sh post

production_release:
<<: *defaults
Expand All @@ -157,6 +176,8 @@ jobs:
# Make sure the working tree is clean
- run: git reset --hard HEAD
- run: npx lerna publish --force-publish='*' --yes
# Verify that @axe-core/react has all requires files
- run: ./axe-core-npm/packages/react/verify-release.sh post

workflows:
version: 2
Expand All @@ -181,6 +202,9 @@ workflows:
- reporter-earl:
requires:
- lint
- react:
requires:
- lint
- axe-core-test:
requires:
- lint
Expand All @@ -194,6 +218,7 @@ workflows:
- webdriverjs
- webdriverio
- reporter-earl
- react
filters:
branches:
only:
Expand All @@ -208,6 +233,7 @@ workflows:
- webdriverjs
- webdriverio
- reporter-earl
- react
filters:
branches:
only: master
6 changes: 5 additions & 1 deletion .github/axe-linter.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
rules:
# Disables a11y issues for testing
html-has-lang: false
html-has-lang: false
exclude:
- packages/react/CHANGELOG.md
- packages/react/example/**/*
- packages/react/cypress/**/*
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ node_modules
dist
__tests__
coverage

packages/react/example/build
packages/react/test/index.js
packages/react/cypress/screenshots
packages/react/verify-release-*
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"packages": ["packages/*", "axe_core_test"],
"packages": ["packages/*", "packages/react/example", "axe_core_test"],
"version": "4.0.0-pre.0"
}
23 changes: 23 additions & 0 deletions packages/react/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = {
rules: {
'react/prop-types': 'off',
'react/no-find-dom-node': 'off'
},
env: {
browser: true,
node: true,
mocha: true
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2018
},
overrides: [
{
files: 'test/*.js',
rules: {
'no-var': 'off'
}
}
]
};
9 changes: 9 additions & 0 deletions packages/react/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.circleci
.github
cypress
node_modules
.editorconfig
.eslintrc.js
cypress.json
tsconfig.json
!dist
1 change: 1 addition & 0 deletions packages/react/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This product is distributed under the [Mozilla Public License 2.0](https://www.mozilla.org/en-US/MPL/2.0/)
114 changes: 114 additions & 0 deletions packages/react/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# @axe-core/react

Test your React application with the [axe-core](https://github.com/dequelabs/axe-core) accessibility testing library. Results will show in the Chrome DevTools console.

Previous versions of this program were maintained at [dequelabs/react-axe](https://github.com/dequelabs/react-axe).

## Usage

[![Greenkeeper badge](https://badges.greenkeeper.io/dequelabs/react-axe.svg)](https://greenkeeper.io/)

Install the module from NPM or elsewhere

```sh
npm install --save-dev @axe-core/react
```

## Initialize the module

Call the exported function passing in the React and ReactDOM objects as well as a timing delay in milliseconds that will be observed between each component change and the time the analysis starts.

```js
const React = require('react');
const ReactDOM = require('react-dom');

if (process.env.NODE_ENV !== 'production') {
const axe = require('@axe-core/react');
axe(React, ReactDOM, 1000);
}
```

Be sure to only run the module in your development environment (as shown in the code above) or else your application will use more resources than necessary when in production. You can use [envify](https://www.npmjs.com/package/envify) to do this as is shown in the [example](./example/package.json#L35).

Once initialized, the module will output accessibility defect information to the Chrome Devtools console every time a component updates.

## Deduplicating

@axe-core/react will deduplicate violations using the rule that raised the violation and the CSS selector and the failureSummary of the specific node. This will ensure that each unique issue will only be printed to the console once.

## Debouncing

The third argument to the exported function is the number of milliseconds to wait for component updates to cease before performing an analysis of all the changes. The changes will be batched and analyzed from the closest common ancestor of all the components that changed within the batch. This generally leads to the first analysis for a dynamic application, analyzing the entire page (which is what you want), while subsequent updates will only analyze a portion of the page (which is probably also what you want).

## Shadow DOM

With version 3.0.0, @axe-core/react now runs accessibility tests inside of open Shadow DOM. You don't have to do anything special other than run @axe-core/react on an component encapsulated with open Shadow DOM (as opposed to closed). For more information, see the [axe-core repo](https://github.com/dequelabs/axe-core).

## Configuration

There is a fourth optional argument that is a configuration object for axe-core. Read about the object here: https://github.com/dequelabs/axe-core/blob/master/doc/API.md#api-name-axeconfigure

```js
const config = {
rules: [
{
id: 'skip-link',
enabled: true
}
]
};

axe(React, ReactDOM, 1000, config);
```

Axe-core's context object can be given as a fifth optional argument to specify which element should (and which should not) be tested. Read more from the Axe-core documentation: https://github.com/dequelabs/axe-core/blob/master/doc/API.md#context-parameter

```js
const context = {
include: [['#preview']]
};

axe(React, ReactDOM, 1000, undefined, context);
```

## Run the example

Run a build in the example directory and start a server to see React-aXe in action in the Chrome Devtools console (opens on localhost:8888):

```sh
npm install
cd example
npm install
npm install -g http-server
npm start
```

## Run the tests

Install dependencies in the root directory (which also installs them in the example directory) and then run the tests:

```
npm install
npm test
```

To debug tests in the Cypress application:

```
npm run test:debug
```

## Compatibility

react-axe uses advanced console logging features and works best in the Chrome browser, with limited functionality in Safari and Firefox.

## Advantages

I have been asked how this is different from modules like react-a11y which test the jsx.

The main difference is that react-axe tests the accessibility of the rendered DOM. This is important because many accessibility issues exist at the intersection of the DOM and the CSS and unless you have a fully rendered DOM, you will get two sorts of inaccuracies:

1. False negatives because of lacking information. Example is in order to test color contrast you must know the foreground and background colors, and
1. False positives because the element being evaluated is not in its final state and the information to communicate this to the testing algorithm is not available. Example is an inert piece of code that will be augmented once it becomes active.

If you have nice clean code, number 2 will be negligible but number 1 will always be a concern.
32 changes: 32 additions & 0 deletions packages/react/after.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
let restoreFunctions: Function[] = [];

function after(host: React.Component, name: string, cb: Function): void {
const originalFn: Function = host[name];
let restoreFn: () => void;

if (originalFn) {
host[name] = function(...args): void {
originalFn.apply(this, args);
cb(host);
};
restoreFn = function(): void {
host[name] = originalFn;
};
} else {
host[name] = function(): void {
cb(host);
};
restoreFn = function(): void {
delete host[name];
};
}

restoreFunctions.push(restoreFn);
}

after.restorePatchedMethods = function(): void {
restoreFunctions.forEach(restoreFn => restoreFn());
restoreFunctions = [];
};

export = after;
6 changes: 6 additions & 0 deletions packages/react/cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"fileServerFolder": "example",
"pluginsFile": false,
"video": false,
"defaultCommandTimeout": 10000
}
5 changes: 5 additions & 0 deletions packages/react/cypress/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
66 changes: 66 additions & 0 deletions packages/react/cypress/integration/index-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable no-undef */
import React from 'react';
import ReactDOM from 'react-dom';
import axe from '../../dist/index';

function filterLogs(args, type) {
let filtered = [];
args.forEach(function (arg, index) {
if (arg.length === 2 && arg[1] === type) {
filtered = arg[1];
} else if (arg.length === 6 && arg[4] === type) {
filtered = arg[4];
}
});
return filtered;
}

describe('React-axe', function () {
it('should assert that page content is correct', function () {
cy.visit('http://localhost:8080');
cy.get('h1').should('contain', 'Our services');
});

it('should run axe in the context of the document', function (done) {
cy.visit('http://localhost:8080').then(function (win) {
cy.spy(win.console, 'group');
cy.spy(win.console, 'groupCollapsed');
cy.spy(win.console, 'groupEnd');

axe(React, ReactDOM, 0).then(function () {
expect(win.console.group).to.be.calledWith(
'%cNew axe issues',
'color:#d93251;font-weight:normal;'
);
expect(win.console.groupCollapsed).to.be.calledWith('%c%s: %c%s %s');
expect(win.console.groupEnd).to.be.called;
done();
});
});
});

it('should run axe inside of Shadow DOM', function (done) {
cy.visit('http://localhost:8080').then(function (win) {
const groupCollapsed = cy.spy(win.console, 'groupCollapsed');
const colorMessage = 'Elements must have sufficient color contrast';

let serviceChooser;
cy.document()
.shadowGet('#service-chooser')
.shadowFirst()
.then(function (node) {
serviceChooser = node[0];
});

axe(React, ReactDOM, 0).then(function () {
expect(filterLogs(groupCollapsed.args, colorMessage)).to.equal(
colorMessage
);
expect(filterLogs(groupCollapsed.args, serviceChooser)).to.equal(
serviceChooser
);
done();
});
});
});
});
Loading

0 comments on commit ea3b15a

Please sign in to comment.