pa11y-ci-reporter-runner 1.0.0 → 2.0.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Pa11y CI Reporter Runner
2
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 transformation of results and configuration data. Functionally, it's an emulation of the Pa11y CI side of the reporter interface with explicit control over execution of each step.
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 performs the Pa11y CI calls to the reporter, including proper transformation of results and configuration data. Functionally, it's an emulation of the Pa11y CI side of the reporter interface with explicit control over execution of each step.
4
4
 
5
5
  ## Installation
6
6
 
@@ -12,7 +12,7 @@ npm install pa11y-ci-reporter-runner
12
12
 
13
13
  ## Usage
14
14
 
15
- Pa11y CI Reporter Runner exports a factory function that creates a reporter runner. This function has four arguments:
15
+ Pa11y CI Reporter Runner exports a factory function `createRunner` that creates a reporter runner. This function takes four arguments:
16
16
 
17
17
  - `resultsFileName`: Path to a Pa11y CI JSON results file.
18
18
  - `reporterName`: Name of the reporter to execute. Can be an npm module (e.g. `pa11y-ci-reporter-html`) or a path to a reporter file.
@@ -62,7 +62,7 @@ The reporter runner has five control functions:
62
62
  - `targetState`: The target state of the runner (any state above except `init`, or any valid value of the `RunnerStates` enum).
63
63
  - `targetUrl`: An optional target URL. If no URL is specified, the runner will stop at the first instance of the target state.
64
64
  - `runUntilNext(targetState, targetUrl)`: Provides the same functionality as `runUntil`, but execution ends at the state prior to the target state/URL, so that the target will execute if `runNext()` is subsequently called.
65
- - `reset()`: Resets the runner to the `init` state. This can be sent from any state.
65
+ - `reset()`: Resets the runner to the `init` state and re-initializes the reporter. This can be sent from any state.
66
66
 
67
67
  These command are all asynchronous and must be completed before another is sent, otherwise an error will be thrown. In addition, once a run has been completed and the runner is in the `afterAll` state it must be `reset` before accepting any run command.
68
68
 
@@ -71,46 +71,59 @@ These command are all asynchronous and must be completed before another is sent,
71
71
  A complete example is provided below:
72
72
 
73
73
  ```js
74
- const { createRunner, RunnerStates } = require("pa11y-ci-reporter-runner");
74
+ const { createRunner, RunnerStates } = require('pa11y-ci-reporter-runner');
75
75
 
76
- const resultsFileName = "pa11yci-results.json";
77
- const reporterName = "../test-reporter.js";
78
- const reporterOptions = { "isSomething": true };
76
+ const resultsFileName = 'pa11yci-results.json';
77
+ const reporterName = '../test-reporter.js';
78
+ const reporterOptions = { isSomething: true };
79
79
  const config = {
80
- defaults: {
81
- timeout: 30000,
82
- },
83
- urls: [
84
- "http://localhost:8080/page1-with-errors.html",
85
- "http://localhost:8080/page1-no-errors.html",
86
- {
87
- url: "https://pa11y.org/timed-out.html",
88
- timeout: 50,
89
- },
90
- ],
80
+ defaults: {
81
+ timeout: 30000
82
+ },
83
+ urls: [
84
+ 'http://localhost:8080/page1-with-errors.html',
85
+ 'http://localhost:8080/page1-no-errors.html',
86
+ {
87
+ url: 'https://pa11y.org/timed-out.html',
88
+ timeout: 50
89
+ }
90
+ ]
91
91
  };
92
92
 
93
93
  test('test all reporter functions', async () => {
94
- const runner = createRunner(resultsFileName, reporterName, reporterOptions, config);
94
+ const runner = createRunner(
95
+ resultsFileName,
96
+ reporterName,
97
+ reporterOptions,
98
+ config
99
+ );
95
100
 
96
- await runner.runAll();
101
+ await runner.runAll();
97
102
 
98
- // Test reporter results
103
+ // Test reporter results
99
104
  });
100
105
 
101
106
  test('test reporter at urlResults state', async () => {
102
- const runner = createRunner(resultsFileName, reporterName, reporterOptions, config);
103
-
104
- await runner.runUntil(RunnerStates.beginUrl, 'http://localhost:8080/page1-no-errors.html');
105
- let currentState = runner.getCurrentState();
106
- // { state: "beginUrl", url: "http://localhost:8080/page1-no-errors.html" }
107
- const nextState = runner.getNextState();
108
- // { state: "urlResults", url: "http://localhost:8080/page1-no-errors.html" }
109
- await runner.runNext();
110
- currentState = runner.getCurrentState();
111
- // { state: "urlResults", url: "http://localhost:8080/page1-no-errors.html" }
112
-
113
- // Test reporter results
107
+ const runner = createRunner(
108
+ resultsFileName,
109
+ reporterName,
110
+ reporterOptions,
111
+ config
112
+ );
113
+
114
+ await runner.runUntil(
115
+ RunnerStates.beginUrl,
116
+ 'http://localhost:8080/page1-no-errors.html'
117
+ );
118
+ let currentState = runner.getCurrentState();
119
+ // { state: "beginUrl", url: "http://localhost:8080/page1-no-errors.html" }
120
+ const nextState = runner.getNextState();
121
+ // { state: "urlResults", url: "http://localhost:8080/page1-no-errors.html" }
122
+ await runner.runNext();
123
+ currentState = runner.getCurrentState();
124
+ // { state: "urlResults", url: "http://localhost:8080/page1-no-errors.html" }
125
+
126
+ // Test reporter results
114
127
  });
115
128
  ```
116
129
 
package/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  'use strict';
2
3
 
3
4
  /**
@@ -21,7 +22,7 @@ const RunnerStates = require('./lib/runner-states');
21
22
  * @param {string} fileName Pa11y CI JSON results file name.
22
23
  * @returns {object} Pa11y CI results.
23
24
  */
24
- const loadPa11yciResults = fileName => {
25
+ const loadPa11yciResults = (fileName) => {
25
26
  if (typeof fileName !== 'string') {
26
27
  throw new TypeError('fileName must be a string');
27
28
  }
@@ -29,8 +30,7 @@ const loadPa11yciResults = fileName => {
29
30
  try {
30
31
  const results = JSON.parse(fs.readFileSync(fileName, 'utf8'));
31
32
  return formatter.convertJsonToResultsObject(results);
32
- }
33
- catch (error) {
33
+ } catch (error) {
34
34
  throw new Error(`Error loading results file - ${error.message}`);
35
35
  }
36
36
  };
@@ -44,16 +44,14 @@ const loadPa11yciResults = fileName => {
44
44
  * @param {object[]} values The urls array from configuration.
45
45
  * @returns {string[]} Array of URL strings.
46
46
  */
47
- const getUrlList = values => {
47
+ const getUrlList = (values) => {
48
48
  const result = [];
49
49
  for (const value of values) {
50
50
  if (typeof value === 'string') {
51
51
  result.push(value);
52
- }
53
- else if (typeof value.url === 'string') {
52
+ } else if (typeof value.url === 'string') {
54
53
  result.push(value.url);
55
- }
56
- else {
54
+ } else {
57
55
  throw new TypeError('invalid url element');
58
56
  }
59
57
  }
@@ -77,9 +75,13 @@ const validateUrls = (results, config) => {
77
75
  }
78
76
  const resultUrls = Object.keys(results.results);
79
77
  const configUrls = getUrlList(config.urls);
80
- if (resultUrls.length !== configUrls.length ||
81
- JSON.stringify(resultUrls.sort()) !== JSON.stringify(configUrls.sort())) {
82
- throw new TypeError('config.urls is specified and does not match results');
78
+ if (
79
+ resultUrls.length !== configUrls.length ||
80
+ JSON.stringify(resultUrls.sort()) !== JSON.stringify(configUrls.sort())
81
+ ) {
82
+ throw new TypeError(
83
+ 'config.urls is specified and does not match results'
84
+ );
83
85
  }
84
86
  };
85
87
 
@@ -90,7 +92,8 @@ const validateUrls = (results, config) => {
90
92
  * @param {object} results Pa11y results for a single URL.
91
93
  * @returns {boolean} True if the results are an execution error.
92
94
  */
93
- const isError = results => results.length === 1 && results[0] instanceof Error;
95
+ const isError = (results) =>
96
+ results.length === 1 && results[0] instanceof Error;
94
97
 
95
98
  /**
96
99
  * Factory to create a pa11y-ci reporter runner that can execute
@@ -105,15 +108,34 @@ const isError = results => results.length === 1 && results[0] instanceof Error;
105
108
  * @returns {object} A Pa11y CI reporter runner.
106
109
  */
107
110
  // eslint-disable-next-line max-lines-per-function
108
- const createRunner = (resultsFileName, reporterName, options = {}, config = {}) => {
111
+ const createRunner = (
112
+ resultsFileName,
113
+ reporterName,
114
+ options = {},
115
+ config = {}
116
+ ) => {
109
117
  const pa11yciResults = loadPa11yciResults(resultsFileName);
110
-
111
118
  validateUrls(pa11yciResults, config);
112
-
113
119
  const pa11yciConfig = createConfig(config);
114
- const reporter = reporterBuilder.buildReporter(reporterName, options, pa11yciConfig.defaults);
115
120
  const urls = config.urls || Object.keys(pa11yciResults.results);
116
121
 
122
+ /**
123
+ * Create a new reporter with the given options and config
124
+ * (encapsulated as a function for consistency).
125
+ *
126
+ * @private
127
+ * @returns {object} The reporter associated with the runner.
128
+ */
129
+ const getReporter = () =>
130
+ reporterBuilder.buildReporter(
131
+ reporterName,
132
+ options,
133
+ pa11yciConfig.defaults
134
+ );
135
+
136
+ // Get the initial reporter
137
+ let reporter = getReporter();
138
+
117
139
  /**
118
140
  * Implements the runner beforeAll event, calling reporter.beforeAll.
119
141
  *
@@ -148,7 +170,13 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
148
170
  const urlConfig = pa11yciConfig.getConfigForUrl(url);
149
171
  await (isError(results)
150
172
  ? reporter.error(results[0], url, urlConfig)
151
- : reporter.results(formatter.getPa11yResultsFromPa11yCiResults(url, pa11yciResults), urlConfig));
173
+ : reporter.results(
174
+ formatter.getPa11yResultsFromPa11yCiResults(
175
+ url,
176
+ pa11yciResults
177
+ ),
178
+ urlConfig
179
+ ));
152
180
  };
153
181
 
154
182
  /**
@@ -171,10 +199,13 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
171
199
 
172
200
  // URLs for the service is always the array of result URLs since they
173
201
  // are used to retrieve the results.
174
- const service = serviceFactory(Object.keys(pa11yciResults.results), actions);
202
+ const service = serviceFactory(
203
+ Object.keys(pa11yciResults.results),
204
+ actions
205
+ );
175
206
 
176
207
  /**
177
- * Resets the runner to the initial state.
208
+ * Resets the runner and reporter to the initial states.
178
209
  *
179
210
  * @async
180
211
  * @public
@@ -182,6 +213,7 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
182
213
  */
183
214
  const reset = async () => {
184
215
  await service.reset();
216
+ reporter = getReporter();
185
217
  };
186
218
 
187
219
  /**
package/lib/config.js CHANGED
@@ -37,7 +37,9 @@ const configFactory = (config) => {
37
37
  }
38
38
 
39
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);
40
+ const result = urls.find(
41
+ (urlObject) => urlObject === url || urlObject.url === url
42
+ );
41
43
  if (typeof result === 'string') {
42
44
  return defaults;
43
45
  }
package/lib/formatter.js CHANGED
@@ -6,8 +6,10 @@
6
6
  * @module formatter
7
7
  */
8
8
 
9
- const isError = issues => issues.length === 1 && Object.keys(issues[0]).length === 1
10
- && issues[0].message;
9
+ const isError = (issues) =>
10
+ issues.length === 1 &&
11
+ Object.keys(issues[0]).length === 1 &&
12
+ issues[0].message;
11
13
 
12
14
  /**
13
15
  * Converts Pa11y CI JSON output to an equivalent Pa11y CI object,
@@ -18,7 +20,7 @@ const isError = issues => issues.length === 1 && Object.keys(issues[0]).length =
18
20
  * @param {object} jsonResults Pa11y CI JSON results.
19
21
  * @returns {object} The equivalent Pa11y CI object.
20
22
  */
21
- const convertJsonToResultsObject = jsonResults => {
23
+ const convertJsonToResultsObject = (jsonResults) => {
22
24
  const results = {
23
25
  total: jsonResults.total,
24
26
  passes: jsonResults.passes,
@@ -28,7 +30,9 @@ const convertJsonToResultsObject = jsonResults => {
28
30
 
29
31
  for (const url of Object.keys(jsonResults.results)) {
30
32
  const issues = jsonResults.results[url];
31
- const formattedIssues = isError(issues) ? [new Error(issues[0].message)] : issues;
33
+ const formattedIssues = isError(issues)
34
+ ? [new Error(issues[0].message)]
35
+ : issues;
32
36
  results.results[url] = formattedIssues;
33
37
  }
34
38
 
@@ -59,4 +63,5 @@ const getPa11yResultsFromPa11yCiResults = (url, results) => {
59
63
  };
60
64
 
61
65
  module.exports.convertJsonToResultsObject = convertJsonToResultsObject;
62
- module.exports.getPa11yResultsFromPa11yCiResults = getPa11yResultsFromPa11yCiResults;
66
+ module.exports.getPa11yResultsFromPa11yCiResults =
67
+ getPa11yResultsFromPa11yCiResults;
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
-
4
3
  /**
5
4
  * State machine for pa11yci.
6
5
  *
@@ -69,8 +68,7 @@ const pa11yciMachine = createMachine(
69
68
  },
70
69
  guards: {
71
70
  hasUrls: (context) => context.urls.length > 0,
72
- isLastUrl: (context) =>
73
- context.urlIndex === context.urls.length - 1
71
+ isLastUrl: (context) => context.urlIndex === context.urls.length - 1
74
72
  }
75
73
  }
76
74
  );
@@ -45,7 +45,7 @@ const StateTypes = Object.freeze({
45
45
  * @param {Array} urls Array of URLs.
46
46
  * @returns {object} The initial state machine context.
47
47
  */
48
- const getInitialContext = urls => ({
48
+ const getInitialContext = (urls) => ({
49
49
  urlIndex: 0,
50
50
  urls
51
51
  });
@@ -57,7 +57,8 @@ const getInitialContext = urls => ({
57
57
  * @param {string} state The state to check.
58
58
  * @returns {boolean} True if the state has an associated url.
59
59
  */
60
- const hasUrl = state => state === RunnerStates.beginUrl || state === RunnerStates.urlResults;
60
+ const hasUrl = (state) =>
61
+ state === RunnerStates.beginUrl || state === RunnerStates.urlResults;
61
62
 
62
63
  /**
63
64
  * Gets the URL for the given machine state.
@@ -66,8 +67,10 @@ const hasUrl = state => state === RunnerStates.beginUrl || state === RunnerState
66
67
  * @param {object} machineState The machine state to check against.
67
68
  * @returns {string} The current URL for the machine state.
68
69
  */
69
- const getUrlForState = machineState => hasUrl(machineState.value)
70
- ? machineState.context.urls[machineState.context.urlIndex] : undefined;
70
+ const getUrlForState = (machineState) =>
71
+ hasUrl(machineState.value)
72
+ ? machineState.context.urls[machineState.context.urlIndex]
73
+ : undefined;
71
74
 
72
75
  /**
73
76
  * Validates that the given state is a valid RunnerStates value. Throws if not.
@@ -92,8 +95,10 @@ const validateRunnerState = (state) => {
92
95
  * @returns {boolean} True if the context matches the target, otherwise false.
93
96
  */
94
97
  const isAtTarget = (machineState, targetState, targetUrl) => {
95
- return machineState.value === targetState &&
96
- (!targetUrl || getUrlForState(machineState) === targetUrl);
98
+ return (
99
+ machineState.value === targetState &&
100
+ (!targetUrl || getUrlForState(machineState) === targetUrl)
101
+ );
97
102
  };
98
103
 
99
104
  /**
@@ -103,7 +108,7 @@ const isAtTarget = (machineState, targetState, targetUrl) => {
103
108
  * @param {object} machineState The machine state.
104
109
  * @returns {object} The machine state summary.
105
110
  */
106
- const getStateSummary = machineState => ({
111
+ const getStateSummary = (machineState) => ({
107
112
  state: machineState.value,
108
113
  url: getUrlForState(machineState)
109
114
  });
@@ -135,10 +140,14 @@ const serviceFactory = (urls, actions) => {
135
140
  */
136
141
  const validateCommandAllowed = () => {
137
142
  if (pendingCommand) {
138
- throw new Error('runner cannot accept a command while another command is pending, await previous command');
143
+ throw new Error(
144
+ 'runner cannot accept a command while another command is pending, await previous command'
145
+ );
139
146
  }
140
147
  if (currentState.value === finalState) {
141
- throw new Error(`runner must be reset before executing any other functions from the ${finalState} state`);
148
+ throw new Error(
149
+ `runner must be reset before executing any other functions from the ${finalState} state`
150
+ );
142
151
  }
143
152
  };
144
153
 
@@ -165,10 +174,12 @@ const serviceFactory = (urls, actions) => {
165
174
  // Check next state and save for reference (machine.transition
166
175
  // is a pure function, so only retrieves the state)
167
176
  if (currentState.value !== finalState) {
168
- nextState = machine.transition(currentState, MachineEvents.NEXT);
177
+ nextState = machine.transition(
178
+ currentState,
179
+ MachineEvents.NEXT
180
+ );
169
181
  }
170
- }
171
- finally {
182
+ } finally {
172
183
  // Ensure pending command is reset in all cases, including on error
173
184
  pendingCommand = false;
174
185
  }
@@ -206,7 +217,8 @@ const serviceFactory = (urls, actions) => {
206
217
  * @param {StateTypes} stateType The state type.
207
218
  * @returns {object} The state object for teh given type.
208
219
  */
209
- const getState = stateType => stateType === StateTypes.current ? currentState : nextState;
220
+ const getState = (stateType) =>
221
+ stateType === StateTypes.current ? currentState : nextState;
210
222
 
211
223
  /**
212
224
  * Common function for executing runUntil and runUntilNext using the
@@ -226,8 +238,10 @@ const serviceFactory = (urls, actions) => {
226
238
  validateRunnerState(targetState);
227
239
 
228
240
  // Run until target or final state is achieved
229
- while (!isAtTarget(getState(stateType), targetState, targetUrl)
230
- && (getState(stateType)).value !== finalState) {
241
+ while (
242
+ !isAtTarget(getState(stateType), targetState, targetUrl) &&
243
+ getState(stateType).value !== finalState
244
+ ) {
231
245
  await sendEvent(MachineEvents.NEXT);
232
246
  }
233
247
 
@@ -235,7 +249,9 @@ const serviceFactory = (urls, actions) => {
235
249
  // not in the results and throw to indicate the command failed.
236
250
  if (!isAtTarget(getState(stateType), targetState, targetUrl)) {
237
251
  const urlString = targetUrl ? ` for targetUrl "${targetUrl}"` : '';
238
- throw new Error(`targetState "${targetState}"${urlString} was not found`);
252
+ throw new Error(
253
+ `targetState "${targetState}"${urlString} was not found`
254
+ );
239
255
  }
240
256
  };
241
257
 
@@ -5,12 +5,11 @@ const path = require('path');
5
5
  const reporterMethods = ['beforeAll', 'begin', 'results', 'error', 'afterAll'];
6
6
  const noop = () => {};
7
7
 
8
- const loadReporter = reporterName => {
8
+ const loadReporter = (reporterName) => {
9
9
  try {
10
10
  // eslint-disable-next-line node/global-require
11
11
  return require(reporterName);
12
- }
13
- catch {
12
+ } catch {
14
13
  // eslint-disable-next-line node/global-require
15
14
  return require(path.resolve(process.cwd(), reporterName));
16
15
  }
@@ -44,8 +43,7 @@ const buildReporter = (reporterName, options, config) => {
44
43
  }
45
44
  }
46
45
  return reporter;
47
- }
48
- catch (error) {
46
+ } catch (error) {
49
47
  throw new Error(`Error loading reporter: ${error.message}`);
50
48
  }
51
49
  };
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "pa11y-ci-reporter-runner",
3
- "version": "1.0.0",
3
+ "version": "2.0.1",
4
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
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "jest --ci",
7
+ "hooks-pre-commit": "npm run lint && npm run prettier-check",
8
+ "hooks-pre-push": "npm audit --audit-level=critical && npm test",
9
+ "lint": "npm run lint-js && npm run lint-md",
8
10
  "lint-js": "eslint .",
9
11
  "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
+ "prettier-check": "prettier --check .",
13
+ "prettier-fix": "prettier --write .",
14
+ "test": "jest --ci"
12
15
  },
13
16
  "repository": {
14
17
  "type": "git",
@@ -24,7 +27,7 @@
24
27
  "author": "Aaron Goldenthal <npm@aarongoldenthal.com>",
25
28
  "license": "MIT",
26
29
  "engines": {
27
- "node": "^12.20.0 || ^14.15.0 || >=16.0.0"
30
+ "node": "^14.15.0 || ^16.13.0 || >=18.0.0"
28
31
  },
29
32
  "files": [
30
33
  "index.js",
@@ -33,17 +36,18 @@
33
36
  "bugs": {
34
37
  "url": "https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner/issues"
35
38
  },
36
- "homepage": "https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner#readme",
39
+ "homepage": "https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner",
37
40
  "devDependencies": {
38
- "@aarongoldenthal/eslint-config-standard": "^12.0.2",
39
- "eslint": "^8.11.0",
40
- "jest": "^27.5.1",
41
- "jest-junit": "^13.0.0",
42
- "markdownlint-cli": "^0.31.1",
43
- "pa11y-ci-reporter-cli-summary": "^1.0.1"
41
+ "@aarongoldenthal/eslint-config-standard": "^15.0.0",
42
+ "eslint": "^8.21.0",
43
+ "jest": "^28.1.3",
44
+ "jest-junit": "^14.0.0",
45
+ "markdownlint-cli": "^0.32.1",
46
+ "pa11y-ci-reporter-cli-summary": "^2.0.0",
47
+ "prettier": "^2.7.1"
44
48
  },
45
49
  "dependencies": {
46
50
  "lodash": "^4.17.21",
47
- "xstate": "^4.30.6"
51
+ "xstate": "^4.33.0"
48
52
  }
49
53
  }