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
JavaScript
/* eslint-disable max-lines -- over 9 lines */
;
/**
* 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;