pa11y-ci-reporter-runner 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Aaron Goldenthal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Pa11y CI Reporter Runner
2
+
3
+ Pa11y CI Reporter Runner is designed to facilitate testing of [Pa11y CI reporters](https://github.com/pa11y/pa11y-ci#write-a-custom-reporter). Given a Pa11y CI JSON results file and optional configuration it simulates the Pa11y CI calls to the reporter, including proper tranformation of results and configuration data. Functionally, it's a mock of the Pa11y CI side of the reporter interface.
4
+
5
+ ## Installation
6
+
7
+ Install Pa11y CI Reporter Runner via [npm](https://www.npmjs.com/package/pa11y-ci-reporter-runner).
8
+
9
+ ```sh
10
+ npm install pa11y-ci-reporter-runner
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Pa11y CI Reporter Runner exports a factory function that creates a reporter runner. This function has four arguments:
16
+
17
+ - `resultsFileName`: Path to a Pa11y CI JSON results file.
18
+ - `reporterName`: Name of the reporter to execute. Can be a dependency (e.g. `pa11y-ci-reporter-html`) or a path to a reporter file.
19
+ - `options`: Optional [reporter options](https://github.com/pa11y/pa11y-ci#reporter-options).
20
+ - `config`: Optional Pa11y CI configuration file that produces the Pa11y CI JSON results.
21
+
22
+ The reporter runner currently has one function:
23
+
24
+ - `runAll`: Simulates Pa11y CI running the complete analysis from the provided JSON results file, calling all associated reporter functions (`beforeAll`, `begin` and `results`/`error` for each URL, `afterAll`).
25
+
26
+ A complete example is provided below:
27
+
28
+ ```js
29
+ const createRunner = require("pa11y-ci-reporter-runner");
30
+
31
+ const resultsFileName = "pa11yci-results.json";
32
+ const reporterName = "../test-reporter.js";
33
+ const reporterOptions = { "isSomething": true };
34
+ const config = {
35
+ defaults: {
36
+ timeout: 30000,
37
+ },
38
+ urls: [
39
+ "http://localhost:8080/page1-with-errors.html",
40
+ "http://localhost:8080/page1-no-errors.html",
41
+ {
42
+ url: "https://pa11y.org/timed-out.html",
43
+ timeout: 50,
44
+ },
45
+ ],
46
+ };
47
+
48
+ test('test reporter', async () => {
49
+ const runner = createRunner(resultsFileName, reporterName, reporterOptions, config);
50
+
51
+ await runner.runAll();
52
+
53
+ // Test reporter results
54
+ });
55
+ ```
56
+
57
+ ## Limitations
58
+
59
+ When passing config to `results`, `error`, and `afterAll`, Pa11y CI Reporter Runner includes the same properties as Pa11y CI except the `browser` property (with the `puppeteer` `browser` object used by Pa11y CI). If the `browser` object is needed, testing should be done with Pa11y CI to ensure proper `browser` configuration.
package/index.js ADDED
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pa11y CI Reporter Runner allows a pa11y-ci reporter to be run
5
+ * from a JSON results file without using pa11y-ci.
6
+ *
7
+ * @module pa11y-ci-reporter-runner
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const formatter = require('./lib/formatter');
12
+ const reporterBuilder = require('./lib/reporterBuilder');
13
+ const createConfig = require('./lib/config');
14
+
15
+ const loadPa11yciResults = fileName => {
16
+ if (typeof fileName !== 'string') {
17
+ throw new TypeError('fileName must be a string');
18
+ }
19
+
20
+ try {
21
+ const results = JSON.parse(fs.readFileSync(fileName, 'utf8'));
22
+ return formatter.convertJsonToResultsObject(results);
23
+ }
24
+ catch (err) {
25
+ throw new Error(`Error loading results file - ${err.message}`);
26
+ }
27
+ };
28
+
29
+ const isError = results => results.length === 1 && results[0] instanceof Error;
30
+
31
+ /**
32
+ * Pa11y CI configuration allows config.urls entries to be either url strings
33
+ * or object with url and other configuration, so this return a list of just
34
+ * the url strings.
35
+ *
36
+ * @private
37
+ * @static
38
+ * @param {object[]} values The urls array from configuration.
39
+ * @returns {string[]} Array of URL strings.
40
+ */
41
+ const getUrlList = values => {
42
+ const result = [];
43
+ for (const value of values) {
44
+ if (typeof value === 'string') {
45
+ result.push(value);
46
+ }
47
+ else if (typeof value.url === 'string') {
48
+ result.push(value.url);
49
+ }
50
+ else {
51
+ throw new TypeError('invalid url element');
52
+ }
53
+ }
54
+ return result;
55
+ };
56
+
57
+ /**
58
+ * Compares URLs in Pa1y CI results and configuration files to ensure
59
+ * consistency (i.e. The same URLs are in both lists, although they
60
+ * may not be in the same order). URLs in config are only checked if
61
+ * provided. If specified and inconsistent, throws error.
62
+ *
63
+ * @private
64
+ * @static
65
+ * @param {object} results Pa11y CI JSON results file.
66
+ * @param {object} config Pa11y CI configuration.
67
+ */
68
+ const validateUrls = (results, config) => {
69
+ // Valid if no urls specified in config
70
+ if (!config.urls) {
71
+ return;
72
+ }
73
+ const resultUrls = Object.keys(results.results);
74
+ const configUrls = getUrlList(config.urls);
75
+ if (resultUrls.length !== configUrls.length ||
76
+ JSON.stringify(resultUrls.sort()) !== JSON.stringify(configUrls.sort())) {
77
+ throw new TypeError('config.urls is specified and does not match results');
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Factory to create a pa11y-ci reporter runner that can execute
83
+ * a reporter with the specified pa11y-ci JSON results file.
84
+ *
85
+ * @public
86
+ * @static
87
+ * @param {string} resultsFileName Pa11y CI JSON results file.
88
+ * @param {string} reporterName Name of the reporter to execute (module or path).
89
+ * @param {object} options The reporter options.
90
+ * @param {object} config The Pa11y CI configuration.
91
+ * @returns {object} A Pa11y CI reporter runner.
92
+ */
93
+ const createRunner = (resultsFileName, reporterName, options = {}, config = {}) => {
94
+ const pa11yciResults = loadPa11yciResults(resultsFileName);
95
+
96
+ validateUrls(pa11yciResults, config);
97
+
98
+ const pa11yciConfig = createConfig(config);
99
+ const reporter = reporterBuilder.buildReporter(reporterName, options, pa11yciConfig.defaults);
100
+
101
+ const runAll = async () => {
102
+ await reporter.beforeAll(config.urls || Object.keys(pa11yciResults.results));
103
+
104
+ for (const url of Object.keys(pa11yciResults.results)) {
105
+ await reporter.begin(url);
106
+
107
+ const urlResults = pa11yciResults.results[url];
108
+ const urlConfig = pa11yciConfig.getConfigForUrl(url);
109
+ if (isError(urlResults)) {
110
+ await reporter.error(urlResults[0], url, urlConfig);
111
+ }
112
+ else {
113
+ await reporter.results(formatter.getPa11yResultsFromPa11yCiResults(url, pa11yciResults), urlConfig);
114
+ }
115
+ }
116
+
117
+ await reporter.afterAll(pa11yciResults, pa11yciConfig.defaults);
118
+ };
119
+
120
+ return {
121
+ runAll
122
+ };
123
+ };
124
+
125
+ module.exports = createRunner;
package/lib/config.js ADDED
@@ -0,0 +1,54 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Manages Pa11y CI configuration.
5
+ *
6
+ * @module config
7
+ */
8
+
9
+ const { defaultsDeep, omit } = require('lodash');
10
+
11
+ const defaultConfig = require('./default-config');
12
+
13
+ const normalizeConfig = (config) => {
14
+ // Remove log and reporters to be consistent with pa11y-ci
15
+ const configDefaults = omit(config.defaults || {}, ['log', 'reporters']);
16
+ return {
17
+ defaults: defaultsDeep({}, configDefaults, defaultConfig),
18
+ urls: config.urls || []
19
+ };
20
+ };
21
+
22
+ /**
23
+ * Factory function that returns a config object for managing PA11y CI
24
+ * configuration including defaults and determining the consolidated
25
+ * configuration for each URL.
26
+ *
27
+ * @static
28
+ * @param {object} config The Pa11y CI configuration.
29
+ * @returns {object} Config object.
30
+ */
31
+ const configFactory = (config) => {
32
+ const { defaults, urls } = normalizeConfig(config);
33
+
34
+ const getConfigForUrl = (url) => {
35
+ if (!urls || urls.length === 0) {
36
+ return defaults;
37
+ }
38
+
39
+ // Config URLs are validated against results, if specified, so will find a result.
40
+ const result = urls.find(urlObject => urlObject === url || urlObject.url === url);
41
+ if (typeof result === 'string') {
42
+ return defaults;
43
+ }
44
+
45
+ return defaultsDeep({}, result, defaults);
46
+ };
47
+
48
+ return {
49
+ defaults,
50
+ getConfigForUrl
51
+ };
52
+ };
53
+
54
+ module.exports = configFactory;
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Default pa11y-ci configuration.
5
+ *
6
+ * @module default-config
7
+ */
8
+
9
+ const defaultColumnWidth = 80;
10
+
11
+ const defaultConfig = {
12
+ wrapWidth: process.stdout.columns || defaultColumnWidth,
13
+ concurrency: 1,
14
+ useIncognitoBrowserContext: true
15
+ };
16
+
17
+ module.exports = defaultConfig;
@@ -0,0 +1,66 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Reformats pa11y-ci JSON data.
5
+ *
6
+ * @module formatter
7
+ */
8
+
9
+ /**
10
+ * Converts Pa11y CI JSON output to an equivalent Pa11y CI object,
11
+ * allowing JSON result files to be used for reporter interface
12
+ * testing. Error messages are converted to Error objects.
13
+ *
14
+ * @static
15
+ * @param {object} jsonResults Pa11y CI JSON results.
16
+ * @returns {object} The equivalent Pa11y CI object.
17
+ */
18
+ const convertJsonToResultsObject = jsonResults => {
19
+ const results = {
20
+ total: jsonResults.total,
21
+ passes: jsonResults.passes,
22
+ errors: jsonResults.errors,
23
+ results: {}
24
+ };
25
+
26
+ for (const url of Object.keys(jsonResults.results)) {
27
+ const issues = jsonResults.results[url];
28
+ let formattedIssues;
29
+ if (issues.length === 1 && Object.keys(issues[0]).length === 1 && issues[0].message) {
30
+ formattedIssues = [new Error(issues[0].message)];
31
+ }
32
+ else {
33
+ formattedIssues = issues;
34
+ }
35
+ results.results[url] = formattedIssues;
36
+ }
37
+
38
+ return results;
39
+ };
40
+
41
+ /**
42
+ * Checks the given Pa11y CI results for the given URL, and if found
43
+ * returns a Pa11y results object of the format sent to the reporter
44
+ * results event. Throws if the url is not found in the results.
45
+ *
46
+ * @static
47
+ * @param {string} url The URL to find results for,.
48
+ * @param {object} results Pa11y CI results object.
49
+ * @returns {object} The Pa11y results object for the URL.
50
+ */
51
+ const getPa11yResultsFromPa11yCiResults = (url, results) => {
52
+ const issues = results.results[url];
53
+ if (!issues) {
54
+ throw new Error(`Results for url "${url} not found`);
55
+ }
56
+
57
+ return {
58
+ documentTitle: `Title for ${url}`,
59
+ pageUrl: url,
60
+ issues
61
+ };
62
+ };
63
+
64
+ module.exports.convertJsonToResultsObject = convertJsonToResultsObject;
65
+ module.exports.getPa11yResultsFromPa11yCiResults = getPa11yResultsFromPa11yCiResults;
66
+
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const reporterMethods = ['beforeAll', 'begin', 'results', 'error', 'afterAll'];
6
+ const noop = () => {};
7
+
8
+ const loadReporter = reporterName => {
9
+ try {
10
+ // eslint-disable-next-line node/global-require
11
+ return require(reporterName);
12
+ }
13
+ catch {
14
+ // eslint-disable-next-line node/global-require
15
+ return require(path.resolve(process.cwd(), reporterName));
16
+ }
17
+ };
18
+
19
+ /**
20
+ * Creates a reporter object for the specified path, following the logic
21
+ * used by pa11y-ci for reporter generation. Unlike pa11y-ci, ensures a
22
+ * function exists for each reporter event to allow manual execution.
23
+ *
24
+ * @param {string} reporterName Name of the reporter to execute
25
+ * (module or path).
26
+ * @param {object} options The reporter options.
27
+ * @param {object} config The Pa11y CI configuration.
28
+ * @returns {object} The reporter object.
29
+ */
30
+ const buildReporter = (reporterName, options, config) => {
31
+ if (typeof reporterName !== 'string') {
32
+ throw new TypeError('reporterName must be a string');
33
+ }
34
+
35
+ try {
36
+ let reporter = loadReporter(reporterName);
37
+ if (typeof reporter === 'function') {
38
+ reporter = reporter(options, config);
39
+ }
40
+ reporterMethods.forEach(method => {
41
+ if (typeof reporter[method] !== 'function') {
42
+ reporter[method] = noop;
43
+ }
44
+ });
45
+ return reporter;
46
+ }
47
+ catch (err) {
48
+ throw new Error(`Error loading reporter: ${err.message}`);
49
+ }
50
+ };
51
+
52
+ module.exports.buildReporter = buildReporter;
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "pa11y-ci-reporter-runner",
3
+ "version": "0.5.0",
4
+ "description": "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.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "jest --ci",
8
+ "lint-js": "eslint .",
9
+ "lint-md": "markdownlint **/*.md --ignore node_modules --ignore Archive",
10
+ "lint": "npm run lint-js && npm run lint-md",
11
+ "push": "npm run lint && npm audit --audit-level=high && npm test"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner.git"
16
+ },
17
+ "keywords": [
18
+ "pa11y-ci",
19
+ "pa11y",
20
+ "reporter",
21
+ "test",
22
+ "testing"
23
+ ],
24
+ "author": "Aaron Goldenthal <npm@aarongoldenthal.com>",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": "^12.20.0 || ^14.15.0 || >=16.0.0"
28
+ },
29
+ "files": [
30
+ "index.js",
31
+ "lib/"
32
+ ],
33
+ "bugs": {
34
+ "url": "https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/issues"
35
+ },
36
+ "homepage": "https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner#readme",
37
+ "devDependencies": {
38
+ "@aarongoldenthal/eslint-config-standard": "^11.0.0",
39
+ "eslint": "^8.8.0",
40
+ "jest": "^27.4.7",
41
+ "jest-junit": "^13.0.0",
42
+ "markdownlint-cli": "^0.30.0",
43
+ "pa11y-ci-reporter-cli-summary": "^1.0.1"
44
+ },
45
+ "dependencies": {
46
+ "lodash": "^4.17.21"
47
+ }
48
+ }