UNPKG

pa11y-ci-reporter-runner

Version:

Pa11y CI Reporter Runner is designed to facilitate testing of Pa11y CI reporters. Given a Pa11y CI JSON results file and optional configuration it simulates the Pa11y CI calls to the reporter.

313 lines (287 loc) 9.35 kB
/* eslint-disable max-lines -- over 9 lines */ 'use strict'; /** * Pa11y CI Reporter Runner allows a Pa11y CI reporter to be run * from a JSON results file without using pa11y-ci. * * @module pa11y-ci-reporter-runner */ const fs = require('node:fs'); const formatter = require('./lib/formatter'); const reporterBuilder = require('./lib/reporter-builder'); const createConfig = require('./lib/config'); const { serviceFactory } = require('./lib/pa11yci-service'); const RunnerStates = require('./lib/runner-states'); /** * Loads the Pa11y CI results from JSON file. * * @param {string} fileName Pa11y CI JSON results file name. * @returns {object} Pa11y CI results. * @throws {TypeError} Throws if fileName is not a string. * @private */ const loadPa11yciResults = (fileName) => { if (typeof fileName !== 'string') { throw new TypeError('fileName must be a string'); } try { // Application requires user-supplied file name // nosemgrep: detect-non-literal-fs-filename, eslint.detect-non-literal-fs-filename const results = JSON.parse(fs.readFileSync(fileName, 'utf8')); return formatter.convertJsonToResultsObject(results); } catch (error) { throw new Error(`Error loading results file - ${error.message}`); } }; /** * Pa11y CI configuration allows config.urls entries to be either url strings * or object with url and other configuration, so this return a list of just * the url strings. * * @param {object[]} values The urls array from configuration. * @returns {string[]} Array of URL strings. * @throws {TypeError} Throws if URLs array contains an invalid * URL element. * @private */ const getUrlList = (values) => { const result = []; for (const value of values) { if (typeof value === 'string') { result.push(value); } else if (typeof value.url === 'string') { result.push(value.url); } else { throw new TypeError('invalid url element'); } } return result; }; /** * Compares URLs in Pa11y CI results and configuration files to ensure * consistency (i.e. The same URLs are in both lists, although they * may not be in the same order). URLs in config are only checked if * provided. If specified and inconsistent, throws error. * * @param {object} results Pa11y CI JSON results file. * @param {object} config Pa11y CI configuration. * @throws {TypeError} Throws if config URLs do not match results URLs. * @private */ const validateUrls = (results, config) => { // Valid if no urls specified in config if (!config.urls) { return; } const resultUrls = Object.keys(results.results); const configUrls = getUrlList(config.urls); if ( resultUrls.length !== configUrls.length || JSON.stringify(resultUrls.sort()) !== JSON.stringify(configUrls.sort()) ) { throw new TypeError( 'config.urls is specified and does not match results' ); } }; /** * Check if the given Pa11y results are an execution error. * * @param {object} results Pa11y results for a single URL. * @returns {boolean} True if the results are an execution error. * @private */ const isError = (results) => results.length === 1 && results[0] instanceof Error; /** * Factory to create a pa11y-ci reporter runner that can execute * a reporter with the specified pa11y-ci JSON results file. * * @param {string} resultsFileName Pa11y CI JSON results file. * @param {string} reporterName Name of the reporter to execute (module or path). * @param {object} options The reporter options. * @param {object} config The Pa11y CI configuration. * @returns {object} A Pa11y CI reporter runner. * @static * @public */ // eslint-disable-next-line max-lines-per-function -- factory function with state const createRunner = ( resultsFileName, reporterName, options = {}, config = {} ) => { const pa11yciResults = loadPa11yciResults(resultsFileName); validateUrls(pa11yciResults, config); const pa11yciConfig = createConfig(config); const urls = config.urls || Object.keys(pa11yciResults.results); /** * Create a new reporter with the given options and config * (encapsulated as a function for consistency). * * @returns {object} The reporter associated with the runner. * @private */ const getReporter = () => reporterBuilder.buildReporter( reporterName, options, pa11yciConfig.defaults ); // Get the initial reporter let reporter = getReporter(); /** * Implements the runner beforeAll event, calling reporter.beforeAll. * * @async * @private */ const beforeAll = async () => { await reporter.beforeAll(urls); }; /** * Implements the runner beginUrl event, calling reporter.begin. * * @param {string} url The url being analyzed. * @async * @private */ const beginUrl = async (url) => { await reporter.begin(url); }; /** * Implements the runner urlResults event, calling reporter.results or * reporter.error as appropriate based on the results. * * @param {string} url The url being analyzed. * @async * @private */ const urlResults = async (url) => { // User supplied input used to index user supplied data file // nosemgrep: eslint.detect-object-injection const results = pa11yciResults.results[url]; const urlConfig = pa11yciConfig.getConfigForUrl(url); await (isError(results) ? reporter.error(results[0], url, urlConfig) : reporter.results( formatter.getPa11yResultsFromPa11yCiResults( url, pa11yciResults ), urlConfig )); }; /** * Implements the runner afterAll event, calling reporter.afterAll. * * @async * @private */ const afterAll = async () => { await reporter.afterAll(pa11yciResults, pa11yciConfig.defaults); }; // Collection of runner actions to be passed to the state service. const actions = { afterAll, beforeAll, beginUrl, urlResults }; // URLs for the service is always the array of result URLs since they // are used to retrieve the results. const service = serviceFactory( Object.keys(pa11yciResults.results), actions ); /** * Resets the runner and reporter to the initial states. * * @instance * @async * @public */ const reset = async () => { await service.reset(); reporter = getReporter(); }; /** * Executes the entire Pa11y CI sequence, calling all reporter functions. * * @instance * @async * @public */ const runAll = async () => { await service.runUntil(RunnerStates.afterAll); }; /** * Executes the next event in the Pa11y CI sequence, calling the * appropriate reporter function. * * @instance * @async * @public */ const runNext = async () => { await service.runNext(); }; /** * Executes the entire Pa11y CI sequence, calling all reporter functions, * until the specified current state and optional URL are reached. If a URL is not * specified, the run completes on the first occurrence of the target state. * * @instance * @param {string} targetState The target state to run to. * @param {string} [targetUrl] The target URL to run to. * @async * @public */ const runUntil = async (targetState, targetUrl) => { await service.runUntil(targetState, targetUrl); }; /** * Executes the entire Pa11y CI sequence, calling all reporter functions, * until the specified next state and optional URL are reached. If a URL is not * specified, the run completes on the first occurrence of the target state. * * @instance * @param {string} targetState The target state to run to. * @param {string} [targetUrl] The target URL to run to. * @async * @public */ const runUntilNext = async (targetState, targetUrl) => { await service.runUntilNext(targetState, targetUrl); }; /** * Get the current state (state, url). * * @instance * @returns {object} The current runner state. * @public */ // eslint-disable-next-line prefer-destructuring -- required for jsdoc const getCurrentState = service.getCurrentState; /** * Get the next state (state, url). * * @instance * @returns {object} The next runner state. * @public */ // eslint-disable-next-line prefer-destructuring -- required for jsdoc const getNextState = service.getNextState; return { getCurrentState, getNextState, reset, runAll, runNext, runUntil, runUntilNext }; }; module.exports.createRunner = createRunner; module.exports.RunnerStates = RunnerStates;