import Proof , { ProofPlugin , ProofTest , TestHookArgs } from '@proof-ui/core' ; import { Browser } from '@proof-ui/browser' ; import CLIPlugin , { CLIOption } from '@proof-ui/cli-plugin' ; import fs from 'fs' ; import * as axe from 'axe-core' ; import { TestConfig } from '@proof-ui/test' ; export interface A11yPluginConfig { config : axe . Spec ; root ? : string ; } type AxeBrowser = Browser & { getAxeReport : ( root : string , config : axe . Spec ) => Promise < any > ; } ; const defaultAxeConfig = { } ; function createMessage ( config , violations ) { let message = '\nAccessibility Errors: \n' ; let total = 0 ; message += `Kind: ${ config . kind } Story: ${ config . story }\n` ; for ( const v of violations ) { message += `Found ${ v . nodes . length } ${ v . id } errors: ${ v . description }\n` ; total += v . nodes . length ; for ( const n of v . nodes ) { if ( n . failureSummary && n . target ) { message += `  ${ n . failureSummary . replace ( '\n  ' , '\n    ' ) }\n` ; message += `    ${ n . target . toString ( ) }\n` ; } } message += '\n' ; } return message ; } async function write ( content , fName ) { return new Promise ( ( resolve , reject ) => { fs . writeFile ( fName , content , ( err ) => { if ( err ) { return reject ( err ) ; } return resolve ( ) ; } ) ; } ) ; } export default class A11yPlugin implements ProofPlugin , CLIPlugin { private enabled = false ; private reportPath = 'proof-a11y.json' ; private readonly root : string = '#root' ; private readonly options : A11yPluginConfig ; constructor ( options ? ) { if ( options ?. root ) { this . root = options . root ; } this . options = options ?? { config : defaultAxeConfig , } ; } apply ( proof ) { if ( ! this . enabled ) { return ; } const testViolations = [ ] ; proof . hooks . testStart . tap ( 'accessibility' , ( t ) => { t . hooks . beforeExecute . tapPromise ( 'accessibility' , async ( _testFunc , testArgs ) => { testArgs . browser . addCommand ( 'getAxeReport' , async ( root , axeConfig ) => { const axePath = require . resolve ( 'axe-core' ) ; const axeSource = fs . readFileSync ( axePath , 'utf8' ) . toString ( ) ; await testArgs . browser . execute ( axeSource ) ; const result = await testArgs . browser . executeAsync ( ( execRoot , execAxeConfig , done ) => { axe . configure ( execAxeConfig ) ; axe . run ( execRoot , ( err , results ) => { if ( err ) done ( err ) ; done ( results ) ; } ) ; } , root , axeConfig ) ; if ( result instanceof Error ) { throw result ; } return result ; } ) ; } ) ; t . hooks . afterExecute . tapPromise ( 'accessibility' , async ( { browser } ) => { const results = await ( browser as AxeBrowser ) . getAxeReport ( this . root , this . options . config ) ; const violations = results ?. violations ; testViolations . push ( { story : t . config . story , kind : t . config . kind , violations : violations || [ ] , } ) ; if ( violations && violations . length > 0 ) { throw new Error ( createMessage ( t . config , violations ) ) ; } } ) ; } ) ; proof . hooks . end . tapPromise ( 'accessibility' , async ( ) => { if ( this . reportPath ) { await write ( JSON . stringify ( testViolations , null , 2 ) , this . reportPath ) ; } } ) ; } command ( ) { return { options : [ { name : 'a11y' , description : 'Run AXE on each test and report accessibility failures.' , type : Boolean , defaultValue : false , } , { name : 'a11y-report' , description : 'The file to write the accessibility report to' , type : String , defaultValue : 'proof-a11y.json' , } , ] , } ; } setArgs ( args ) { if ( args . a11Y ) { this . enabled = true ; } if ( args . a11YReport ) { this . reportPath = args . a11YReport ; } } }