From c856172138918894626b93207368321a059efb53 Mon Sep 17 00:00:00 2001 From: Artem Sapegin Date: Tue, 10 Aug 2021 09:30:10 +0200 Subject: [PATCH] feat: New API Refs #75 --- src/index.ts | 200 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 121 insertions(+), 79 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2ed76da..0504fff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,20 @@ import axe from 'axe-core'; +// TODO: After all handlers +// TODO: Default console reporter + +// https://github.com/cypress-io/code-coverage + +// Send an event we can subscribe to inside a plugin +after(() => { + cy.task('cypressAxeAfterAll'); + console.log('🦜🦜🦜🦜 AFTER 🦜🦜🦜🦜'); +}); + +Cypress.on('test:after:run', () => { + console.log('🦜🦜🦜🦜 test:after:run 🦜🦜🦜🦜'); +}); + declare global { interface Window { axe: typeof axe; @@ -11,16 +26,58 @@ declare global { namespace Cypress { interface Chainable { injectAxe: typeof injectAxe; - configureAxe: typeof configureAxe; + configureCypressAxe: typeof configureCypressAxe; checkA11y: typeof checkA11y; } } } -export interface Options extends axe.RunOptions { - includedImpacts?: string[]; +type Reporter = (results: RunResults[]) => void; + +interface RunResults { + /** Test file */ + filename: string; + /** Label passed to checkA11y() */ + label?: string; + /** All results returned by Axe, including violations and passes */ + results: axe.AxeResults; +} + +interface CypressAxeOptions { + /** Axe options object that is passed to axe.configure() */ + axeOptions?: axe.Spec; + /** Axe run options object that is passed to each axe.run() call */ + axeRunOptions?: axe.RunOptions; + /** Returns true when the assertion should fail */ + shouldFail?: (results: axe.AxeResults) => boolean; + /** Custom reporters */ + reporters?: Reporter[]; } +type CypressAxeRunOptions = Omit; + +let config: CypressAxeOptions = {}; + +const shouldFailDefault = ({ violations }: axe.AxeResults) => + violations.length > 0; + +const formatFailureMessage = (violations: axe.Result[]) => + `${violations.length} accessibility violation${ + violations.length === 1 ? '' : 's' + } ${violations.length === 1 ? 'was' : 'were'} detected`; + +const formatViolationMessage = (violation: axe.Result) => + `${violation.id} on ${violation.nodes.length} element${ + violation.nodes.length === 1 ? '' : 's' + }`; + +const isEmptyObjectorNull = (value: any) => { + if (value == null) { + return true; + } + return Object.entries(value).length === 0 && value.constructor === Object; +}; + export const injectAxe = () => { const fileName = typeof require?.resolve === 'function' @@ -33,94 +90,79 @@ export const injectAxe = () => { ); }; -export const configureAxe = (configurationOptions = {}) => { +export const configureCypressAxe = ({ + axeOptions = {}, + ...options +}: CypressAxeOptions = {}) => { cy.window({ log: false }).then((win) => { - return win.axe.configure(configurationOptions); + config = { ...config, ...options }; + return win.axe.configure(axeOptions); }); }; -function isEmptyObjectorNull(value: any) { - if (value == null) { - return true; - } - return Object.entries(value).length === 0 && value.constructor === Object; -} - const checkA11y = ( context?: axe.ElementContext, - options?: Options, - violationCallback?: (violations: axe.Result[]) => void, - skipFailures = false + options?: CypressAxeRunOptions, + label?: string ) => { - cy.window({ log: false }) - .then((win) => { - if (isEmptyObjectorNull(context)) { - context = undefined; - } - if (isEmptyObjectorNull(options)) { - options = undefined; - } - if (isEmptyObjectorNull(violationCallback)) { - violationCallback = undefined; - } - const { includedImpacts, ...axeOptions } = options || {}; - return win.axe - .run(context || win.document, axeOptions) - .then(({ violations }) => { - return includedImpacts && - Array.isArray(includedImpacts) && - Boolean(includedImpacts.length) - ? violations.filter( - (v) => v.impact && includedImpacts.includes(v.impact) - ) - : violations; + cy.window({ log: false }).then((win) => { + if (isEmptyObjectorNull(context)) { + context = undefined; + } + if (isEmptyObjectorNull(options)) { + options = undefined; + } + // TODO: Merge with global options + const { + axeRunOptions = {}, + shouldFail = shouldFailDefault, + reporters = [], + } = options || {}; + return win.axe + .run(context || win.document, axeRunOptions) + .then((results) => { + // TODO + reporters.forEach((reporter) => { + const filename = ''; // TODO + reporter([{ filename, label, results }]); }); - }) - .then((violations) => { - if (violations.length) { - if (violationCallback) { - violationCallback(violations); + + // If we have any violations, fail the run + if (shouldFail(results)) { + assert.equal( + results.violations.length, + 0, + formatFailureMessage(results.violations) + ); } - violations.forEach((v) => { - const selectors = v.nodes - .reduce((acc, node) => acc.concat(node.target), []) - .join(', '); - - Cypress.log({ - $el: Cypress.$(selectors), - name: 'a11y error!', - consoleProps: () => v, - message: `${v.id} on ${v.nodes.length} Node${ - v.nodes.length === 1 ? '' : 's' - }`, - }); - }); - } - return cy.wrap(violations, { log: false }); - }) - .then((violations) => { - if (!skipFailures) { - assert.equal( - violations.length, - 0, - `${violations.length} accessibility violation${ - violations.length === 1 ? '' : 's' - } ${violations.length === 1 ? 'was' : 'were'} detected` - ); - } else if (violations.length) { - Cypress.log({ - name: 'a11y violation summary', - message: `${violations.length} accessibility violation${ - violations.length === 1 ? '' : 's' - } ${violations.length === 1 ? 'was' : 'were'} detected`, - }); - } - }); + // Log each violation type + if (results.violations.length > 0) { + results.violations.forEach((violation) => { + const selectors = violation.nodes + .reduce((acc, node) => acc.concat(node.target), []) + .join(', '); + + Cypress.log({ + $el: Cypress.$(selectors), + name: 'Accessibility violation', + consoleProps: () => violation, + message: formatViolationMessage(violation), + }); + }); + } + }); + }); }; Cypress.Commands.add('injectAxe', injectAxe); -Cypress.Commands.add('configureAxe', configureAxe); +Cypress.Commands.add('configureCypressAxe', configureCypressAxe); -Cypress.Commands.add('checkA11y', checkA11y); +Cypress.Commands.add( + 'checkA11y', + { + prevSubject: 'optional', + }, + checkA11y +);