pa11y-ci-reporter-runner 0.7.1 → 2.0.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/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 finer control over execution.
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.
@@ -42,16 +42,27 @@ flowchart LR;
42
42
 
43
43
  When calling reporter functions, the runner transforms the Pa11y CI results and configuration data to provide the appropriate arguments. For example, the `results` function is called with the results as returned from Pa11y (slightly different than those returned from Pa11y CI) and the consolidated configuration for the analysis of that URL.
44
44
 
45
+ The runner state can be obtained via the following runner functions:
46
+
47
+ - `getCurrentState()`: The current state of the runner.
48
+ - `getNextState()`: The next state of the runner (i.e. the state that will be obtained by calling the `runNext()` function).
49
+
50
+ Both functions return an object with the following properties:
51
+
52
+ - `state`: The current runner state (any state value shown above)
53
+ - `url`: The current URL for any state with an applicable URL (`beginUrl` and `urlResults`), otherwise `undefined`.
54
+
45
55
  ### Runner Execution
46
56
 
47
- The reporter runner has four control functions:
57
+ The reporter runner has five control functions:
48
58
 
49
59
  - `runAll()`: Simulates Pa11y CI running the analysis from the provided JSON results file from the current state through the end, calling all associated reporter functions.
50
60
  - `runNext()`: Simulates Pa11y CI running through the next state from the provided JSON results file, calling the associated reporter function as noted above.
51
61
  - `runUntil(targetState, targetUrl)`: Simulates Pa11y CI running the analysis from the provided JSON results file from the current state through the specified state/URL, calling the associated reporter functions as noted above. An error will be thrown if the end of the results are reached and the target was not found. This function takes the following arguments:
52
- - `targetState`: The target state of the runner (any state above except `init`).
62
+ - `targetState`: The target state of the runner (any state above except `init`, or any valid value of the `RunnerStates` enum).
53
63
  - `targetUrl`: An optional target URL. If no URL is specified, the runner will stop at the first instance of the target state.
54
- - `reset()`: Resets the runner to the `init` state. This can be sent from any state.
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 and re-initializes the reporter. This can be sent from any state.
55
66
 
56
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.
57
68
 
@@ -60,27 +71,32 @@ These command are all asynchronous and must be completed before another is sent,
60
71
  A complete example is provided below:
61
72
 
62
73
  ```js
63
- const { createRunner, RunnerStates } = require("pa11y-ci-reporter-runner");
74
+ const { createRunner, RunnerStates } = require('pa11y-ci-reporter-runner');
64
75
 
65
- const resultsFileName = "pa11yci-results.json";
66
- const reporterName = "../test-reporter.js";
67
- const reporterOptions = { "isSomething": true };
76
+ const resultsFileName = 'pa11yci-results.json';
77
+ const reporterName = '../test-reporter.js';
78
+ const reporterOptions = { isSomething: true };
68
79
  const config = {
69
80
  defaults: {
70
- timeout: 30000,
81
+ timeout: 30000
71
82
  },
72
83
  urls: [
73
- "http://localhost:8080/page1-with-errors.html",
74
- "http://localhost:8080/page1-no-errors.html",
84
+ 'http://localhost:8080/page1-with-errors.html',
85
+ 'http://localhost:8080/page1-no-errors.html',
75
86
  {
76
- url: "https://pa11y.org/timed-out.html",
77
- timeout: 50,
78
- },
79
- ],
87
+ url: 'https://pa11y.org/timed-out.html',
88
+ timeout: 50
89
+ }
90
+ ]
80
91
  };
81
92
 
82
93
  test('test all reporter functions', async () => {
83
- const runner = createRunner(resultsFileName, reporterName, reporterOptions, config);
94
+ const runner = createRunner(
95
+ resultsFileName,
96
+ reporterName,
97
+ reporterOptions,
98
+ config
99
+ );
84
100
 
85
101
  await runner.runAll();
86
102
 
@@ -88,12 +104,24 @@ test('test all reporter functions', async () => {
88
104
  });
89
105
 
90
106
  test('test reporter at urlResults state', async () => {
91
- const runner = createRunner(resultsFileName, reporterName, reporterOptions, config);
92
-
93
- await runner.runUntil(RunnerStates.beginUrl, 'http://localhost:8080/page1-no-errors.html');
94
- // The runner is now in the beginUrl state for http://localhost:8080/page1-no-errors.html
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" }
95
122
  await runner.runNext();
96
- // The runner is now in the urlResults state for http://localhost:8080/page1-no-errors.html
123
+ currentState = runner.getCurrentState();
124
+ // { state: "urlResults", url: "http://localhost:8080/page1-no-errors.html" }
97
125
 
98
126
  // Test reporter results
99
127
  });
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,18 +108,38 @@ 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
  *
142
+ * @async
120
143
  * @private
121
144
  */
122
145
  const beforeAll = async () => {
@@ -126,6 +149,7 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
126
149
  /**
127
150
  * Implements the runner beginUrl event, calling reporter.begin.
128
151
  *
152
+ * @async
129
153
  * @private
130
154
  * @param {string} url The url being analyzed.
131
155
  */
@@ -137,6 +161,7 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
137
161
  * Implements the runner urlResults event, calling reporter.results or
138
162
  * reporter.error as appropriate based on the results.
139
163
  *
164
+ * @async
140
165
  * @private
141
166
  * @param {string} url The url being analyzed.
142
167
  */
@@ -145,12 +170,19 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
145
170
  const urlConfig = pa11yciConfig.getConfigForUrl(url);
146
171
  await (isError(results)
147
172
  ? reporter.error(results[0], url, urlConfig)
148
- : reporter.results(formatter.getPa11yResultsFromPa11yCiResults(url, pa11yciResults), urlConfig));
173
+ : reporter.results(
174
+ formatter.getPa11yResultsFromPa11yCiResults(
175
+ url,
176
+ pa11yciResults
177
+ ),
178
+ urlConfig
179
+ ));
149
180
  };
150
181
 
151
182
  /**
152
183
  * Implements the runner afterAll event, calling reporter.afterAll.
153
184
  *
185
+ * @async
154
186
  * @private
155
187
  */
156
188
  const afterAll = async () => {
@@ -167,21 +199,27 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
167
199
 
168
200
  // URLs for the service is always the array of result URLs since they
169
201
  // are used to retrieve the results.
170
- const service = serviceFactory(Object.keys(pa11yciResults.results), actions);
202
+ const service = serviceFactory(
203
+ Object.keys(pa11yciResults.results),
204
+ actions
205
+ );
171
206
 
172
207
  /**
173
- * Resets the runner to the initial state.
208
+ * Resets the runner and reporter to the initial states.
174
209
  *
210
+ * @async
175
211
  * @public
176
212
  * @instance
177
213
  */
178
214
  const reset = async () => {
179
215
  await service.reset();
216
+ reporter = getReporter();
180
217
  };
181
218
 
182
219
  /**
183
220
  * Executes the entire Pa11y CI sequence, calling all reporter functions.
184
221
  *
222
+ * @async
185
223
  * @public
186
224
  * @instance
187
225
  */
@@ -193,6 +231,7 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
193
231
  * Executes the next event in the Pa11y CI sequence, calling the
194
232
  * appropriate reporter function.
195
233
  *
234
+ * @async
196
235
  * @public
197
236
  * @instance
198
237
  */
@@ -202,11 +241,12 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
202
241
 
203
242
  /**
204
243
  * Executes the entire Pa11y CI sequence, calling all reporter functions,
205
- * until the specified state and optional URL are reached. If no URL is provided
244
+ * until the specified current state and optional URL are reached. If a URL is not
206
245
  * specified, the run completes on the first occurrence of the target state.
207
246
  *
247
+ * @async
208
248
  * @public
209
- * @interface
249
+ * @instance
210
250
  * @param {string} targetState The target state to run to.
211
251
  * @param {string} [targetUrl] The target URL to run to.
212
252
  */
@@ -214,11 +254,49 @@ const createRunner = (resultsFileName, reporterName, options = {}, config = {})
214
254
  await service.runUntil(targetState, targetUrl);
215
255
  };
216
256
 
257
+ /**
258
+ * Executes the entire Pa11y CI sequence, calling all reporter functions,
259
+ * until the specified next state and optional URL are reached. If a URL is not
260
+ * specified, the run completes on the first occurrence of the target state.
261
+ *
262
+ * @async
263
+ * @public
264
+ * @instance
265
+ * @param {string} targetState The target state to run to.
266
+ * @param {string} [targetUrl] The target URL to run to.
267
+ */
268
+ const runUntilNext = async (targetState, targetUrl) => {
269
+ await service.runUntilNext(targetState, targetUrl);
270
+ };
271
+
272
+ /**
273
+ * Get the current state (state, url).
274
+ *
275
+ * @public
276
+ * @instance
277
+ * @returns {object} The current state.
278
+ */
279
+ // eslint-disable-next-line prefer-destructuring -- required for jsdoc
280
+ const getCurrentState = service.getCurrentState;
281
+
282
+ /**
283
+ * Get the next state (state, url).
284
+ *
285
+ * @public
286
+ * @instance
287
+ * @returns {object} The next state.
288
+ */
289
+ // eslint-disable-next-line prefer-destructuring -- required for jsdoc
290
+ const getNextState = service.getNextState;
291
+
217
292
  return {
293
+ getCurrentState,
294
+ getNextState,
218
295
  reset,
219
296
  runAll,
220
297
  runNext,
221
- runUntil
298
+ runUntil,
299
+ runUntilNext
222
300
  };
223
301
  };
224
302
 
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,7 +1,16 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * State machine for pa11yci.
5
+ *
6
+ * @module pa11yci-machine
7
+ */
8
+
3
9
  const { createMachine, assign } = require('xstate');
4
10
 
11
+ /**
12
+ * State machine for pa11yci.
13
+ */
5
14
  const pa11yciMachine = createMachine(
6
15
  {
7
16
  id: 'pa11yci-runner',
@@ -59,8 +68,7 @@ const pa11yciMachine = createMachine(
59
68
  },
60
69
  guards: {
61
70
  hasUrls: (context) => context.urls.length > 0,
62
- isLastUrl: (context) =>
63
- context.urlIndex === context.urls.length - 1
71
+ isLastUrl: (context) => context.urlIndex === context.urls.length - 1
64
72
  }
65
73
  }
66
74
  );
@@ -1,11 +1,43 @@
1
+ /* eslint-disable max-lines */
1
2
  'use strict';
2
3
 
3
- const { interpret } = require('xstate');
4
- const machine = require('./pa11yci-machine');
4
+ /**
5
+ * Service that interprets pa11yci-machine.
6
+ *
7
+ * @module pa11yci-service
8
+ */
5
9
 
10
+ const machine = require('./pa11yci-machine');
6
11
  const RunnerStates = require('./runner-states');
12
+
7
13
  const finalState = RunnerStates.afterAll;
8
14
 
15
+ /**
16
+ * Enum for machine events.
17
+ *
18
+ * @enum {string}
19
+ * @private
20
+ * @readonly
21
+ * @static
22
+ */
23
+ const MachineEvents = Object.freeze({
24
+ NEXT: 'NEXT',
25
+ RESET: 'RESET'
26
+ });
27
+
28
+ /**
29
+ * Enum for runner service states.
30
+ *
31
+ * @enum {string}
32
+ * @private
33
+ * @readonly
34
+ * @static
35
+ */
36
+ const StateTypes = Object.freeze({
37
+ current: 'current',
38
+ next: 'next'
39
+ });
40
+
9
41
  /**
10
42
  * Gets the initial pa11yci-runner state machine context given an array of URLs.
11
43
  *
@@ -13,25 +45,38 @@ const finalState = RunnerStates.afterAll;
13
45
  * @param {Array} urls Array of URLs.
14
46
  * @returns {object} The initial state machine context.
15
47
  */
16
- const getInitialContext = urls => ({
48
+ const getInitialContext = (urls) => ({
17
49
  urlIndex: 0,
18
50
  urls
19
51
  });
20
52
 
21
53
  /**
22
- * Checks the state to determine whether it has an associated URL.
54
+ * Checks the runner state to determine whether it has an associated URL.
23
55
  *
24
56
  * @private
25
- * @param {RunnerStates} state The state to check.
26
- * @returns {boolean} True if the state has an associated url.
57
+ * @param {string} state The state to check.
58
+ * @returns {boolean} True if the state has an associated url.
27
59
  */
28
- const hasUrl = state => state === RunnerStates.beginUrl || state === RunnerStates.urlResults;
60
+ const hasUrl = (state) =>
61
+ state === RunnerStates.beginUrl || state === RunnerStates.urlResults;
62
+
63
+ /**
64
+ * Gets the URL for the given machine state.
65
+ *
66
+ * @private
67
+ * @param {object} machineState The machine state to check against.
68
+ * @returns {string} The current URL for the machine state.
69
+ */
70
+ const getUrlForState = (machineState) =>
71
+ hasUrl(machineState.value)
72
+ ? machineState.context.urls[machineState.context.urlIndex]
73
+ : undefined;
29
74
 
30
75
  /**
31
76
  * Validates that the given state is a valid RunnerStates value. Throws if not.
32
77
  *
33
78
  * @private
34
- * @param {string} state The state to validate.
79
+ * @param {string} state The state to validate.
35
80
  */
36
81
  const validateRunnerState = (state) => {
37
82
  if (!Object.keys(RunnerStates).includes(state)) {
@@ -40,21 +85,36 @@ const validateRunnerState = (state) => {
40
85
  };
41
86
 
42
87
  /**
43
- * Checks if the context matches the targetState and either no
88
+ * Checks if the machine state matches the targetState and either no
44
89
  * targetUrl was specified or the context matches the targetUrl.
45
90
  *
46
91
  * @private
47
- * @param {object} context The current state machine context.
48
- * @param {string} targetState The target runner state.
49
- * @param {string} [targetUrl] The target URL.
50
- * @returns {boolean} True if the context matches the target, otherwise false.
92
+ * @param {object} machineState The machine state to check against.
93
+ * @param {RunnerStates} targetState The target runner state.
94
+ * @param {string} [targetUrl] The target URL.
95
+ * @returns {boolean} True if the context matches the target, otherwise false.
51
96
  */
52
- const isAtTarget = (context, targetState, targetUrl) => {
53
- return context.state === targetState && (!targetUrl || context.url === targetUrl);
97
+ const isAtTarget = (machineState, targetState, targetUrl) => {
98
+ return (
99
+ machineState.value === targetState &&
100
+ (!targetUrl || getUrlForState(machineState) === targetUrl)
101
+ );
54
102
  };
55
103
 
56
104
  /**
57
- * Factory function that return a pa11yci-runner service.
105
+ * Gets a summary of the machine state (state, url).
106
+ *
107
+ * @private
108
+ * @param {object} machineState The machine state.
109
+ * @returns {object} The machine state summary.
110
+ */
111
+ const getStateSummary = (machineState) => ({
112
+ state: machineState.value,
113
+ url: getUrlForState(machineState)
114
+ });
115
+
116
+ /**
117
+ * Factory function that returns a pa11yci-runner service.
58
118
  *
59
119
  * @public
60
120
  * @static
@@ -65,116 +125,191 @@ const isAtTarget = (context, targetState, targetUrl) => {
65
125
  // eslint-disable-next-line max-lines-per-function
66
126
  const serviceFactory = (urls, actions) => {
67
127
  let pendingCommand;
68
- const currentContext = {
69
- state: undefined,
70
- url: undefined
71
- };
72
-
73
- // Create service from pa11yci-machine with context, setup
74
- // transition event handler, and start service
75
- const service = interpret(machine.withContext(getInitialContext(urls)));
76
- service.onTransition(async (state) => {
77
- // Save the current state and url for use in manual transitions.
78
- // The state machine only increments the url index, so use that
79
- // to get the url from the original urls array.
80
- currentContext.state = state.value;
81
- currentContext.url = hasUrl(currentContext.state)
82
- ? urls[state.context.urlIndex] : undefined;
83
128
 
84
- if (pendingCommand) {
85
- // Do not take any action in init state
86
- if (currentContext.state !== 'init') {
87
- await actions[currentContext.state](currentContext.url);
88
- }
89
- pendingCommand.resolve();
90
- pendingCommand = undefined;
91
- }
92
- });
93
- service.start();
129
+ // Implement custom xstate interpreter, which allows for tracking for current
130
+ // and the next state, which is required for some control functions.
131
+ const pa11yMachine = machine.withContext(getInitialContext(urls));
132
+ let currentState = pa11yMachine.initialState;
133
+ // machine.transition is a pure function, so only retrieves the state
134
+ let nextState = machine.transition(currentState, MachineEvents.NEXT);
94
135
 
95
136
  /**
96
- * Validate that a command is allowed in the given state. Throws if invalid.
137
+ * Validates that a command is allowed in the given state. Throws if invalid.
97
138
  *
98
139
  * @private
99
140
  */
100
141
  const validateCommandAllowed = () => {
101
142
  if (pendingCommand) {
102
- 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
+ );
103
146
  }
104
- if (currentContext.state === finalState) {
105
- throw new Error(`runner must be reset before executing any other functions from the ${finalState} state`);
147
+ if (currentState.value === finalState) {
148
+ throw new Error(
149
+ `runner must be reset before executing any other functions from the ${finalState} state`
150
+ );
106
151
  }
107
152
  };
108
153
 
109
154
  /**
110
- * Sends the specified event to the pa11yci-runner service. Resolves when
111
- * the next state has been reached and the reporter event raised.
155
+ * Sends the specified event to the pa11yci-machine and executes
156
+ * the applicable action for that state.
112
157
  *
158
+ * @async
113
159
  * @private
114
- * @param {string} event The event name to be sent.
115
- * @returns {Promise<void>} Promise that indicates a pending state change.
160
+ * @param {MachineEvents} event The event name to be sent.
116
161
  */
117
- const sendEvent = (event) => {
118
- return new Promise((resolve, reject) => {
119
- pendingCommand = { resolve, reject };
120
- service.send({ type: event });
121
- });
162
+ const sendEvent = async (event) => {
163
+ try {
164
+ // Track pending command to block other commands
165
+ pendingCommand = true;
166
+
167
+ // Send event to the machine and executes the action for the
168
+ // current state (except in init, which has no action)
169
+ currentState = machine.transition(currentState, event);
170
+ if (currentState.value !== 'init') {
171
+ await actions[currentState.value](getUrlForState(currentState));
172
+ }
173
+
174
+ // Check next state and save for reference (machine.transition
175
+ // is a pure function, so only retrieves the state)
176
+ if (currentState.value !== finalState) {
177
+ nextState = machine.transition(
178
+ currentState,
179
+ MachineEvents.NEXT
180
+ );
181
+ }
182
+ } finally {
183
+ // Ensure pending command is reset in all cases, including on error
184
+ pendingCommand = false;
185
+ }
122
186
  };
123
187
 
124
188
  /**
125
189
  * Resets the service to the init state.
126
190
  *
191
+ * @async
127
192
  * @public
128
193
  * @instance
129
194
  */
130
195
  const reset = async () => {
131
- await sendEvent('RESET');
196
+ await sendEvent(MachineEvents.RESET);
132
197
  };
133
198
 
134
199
  /**
135
200
  * Executes the next event in the Pa11y CI sequence, calling the
136
201
  * appropriate reporter function.
137
202
  *
203
+ * @async
138
204
  * @public
139
205
  * @instance
140
206
  */
141
207
  const runNext = async () => {
142
208
  validateCommandAllowed();
143
209
 
144
- await sendEvent('NEXT');
210
+ await sendEvent(MachineEvents.NEXT);
145
211
  };
146
212
 
147
213
  /**
148
- * Executes the entire Pa11y CI sequence, calling all reporter functions,
149
- * until the specified state and optional URL are reached. If no URL is provided
150
- * specified, the run completes on the first occurrence of the target state.
214
+ * Gets the state for the given state type (current or next).
151
215
  *
152
- * @public
153
- * @interface
154
- * @param {string} targetState The target state to run to.
155
- * @param {string} [targetUrl] The target URL to run to.
216
+ * @private
217
+ * @param {StateTypes} stateType The state type.
218
+ * @returns {object} The state object for teh given type.
156
219
  */
157
- const runUntil = async (targetState, targetUrl) => {
220
+ const getState = (stateType) =>
221
+ stateType === StateTypes.current ? currentState : nextState;
222
+
223
+ /**
224
+ * Common function for executing runUntil and runUntilNext using the
225
+ * specified state type to check for completion. Executes the entire
226
+ * Pa11y CI sequence, calling all reporter functions, until the
227
+ * specified state and optional URL are reached. If a URL is not specified,
228
+ * the run completes on the first occurrence of the target state.
229
+ *
230
+ * @async
231
+ * @private
232
+ * @param {StateTypes} stateType The state type.
233
+ * @param {RunnerStates} targetState The target state to run to.
234
+ * @param {string} [targetUrl] The target URL to run to.
235
+ */
236
+ const runUntilInternal = async (stateType, targetState, targetUrl) => {
158
237
  validateCommandAllowed();
159
238
  validateRunnerState(targetState);
160
239
 
161
- while (!isAtTarget(currentContext, targetState, targetUrl)
162
- && currentContext.state !== finalState) {
163
- await sendEvent('NEXT');
240
+ // Run until target or final state is achieved
241
+ while (
242
+ !isAtTarget(getState(stateType), targetState, targetUrl) &&
243
+ getState(stateType).value !== finalState
244
+ ) {
245
+ await sendEvent(MachineEvents.NEXT);
164
246
  }
165
247
 
166
248
  // If the finalState is reached and not at target then it is
167
249
  // not in the results and throw to indicate the command failed.
168
- if (!isAtTarget(currentContext, targetState, targetUrl)) {
250
+ if (!isAtTarget(getState(stateType), targetState, targetUrl)) {
169
251
  const urlString = targetUrl ? ` for targetUrl "${targetUrl}"` : '';
170
- throw new Error(`targetState "${targetState}"${urlString} was not found`);
252
+ throw new Error(
253
+ `targetState "${targetState}"${urlString} was not found`
254
+ );
171
255
  }
172
256
  };
173
257
 
258
+ /**
259
+ * Executes the entire Pa11y CI sequence, calling all reporter functions,
260
+ * until the specified current state and optional URL are reached. If a URL is not
261
+ * specified, the run completes on the first occurrence of the target state.
262
+ *
263
+ * @async
264
+ * @public
265
+ * @instance
266
+ * @param {RunnerStates} targetState The target state to run to.
267
+ * @param {string} [targetUrl] The target URL to run to.
268
+ */
269
+ const runUntil = async (targetState, targetUrl) => {
270
+ await runUntilInternal(StateTypes.current, targetState, targetUrl);
271
+ };
272
+
273
+ /**
274
+ * Executes the entire Pa11y CI sequence, calling all reporter functions,
275
+ * until the specified next state and optional URL are reached. If a URL is not
276
+ * specified, the run completes on the first occurrence of the target state.
277
+ *
278
+ * @async
279
+ * @public
280
+ * @instance
281
+ * @param {RunnerStates} targetState The target state to run to.
282
+ * @param {string} [targetUrl] The target URL to run to.
283
+ */
284
+ const runUntilNext = async (targetState, targetUrl) => {
285
+ await runUntilInternal(StateTypes.next, targetState, targetUrl);
286
+ };
287
+
288
+ /**
289
+ * Get the current state (state, url).
290
+ *
291
+ * @public
292
+ * @instance
293
+ * @returns {object} The current state.
294
+ */
295
+ const getCurrentState = () => getStateSummary(currentState);
296
+
297
+ /**
298
+ * Get the next state (state, url).
299
+ *
300
+ * @public
301
+ * @instance
302
+ * @returns {object} The next state.
303
+ */
304
+ const getNextState = () => getStateSummary(nextState);
305
+
174
306
  return {
307
+ getCurrentState,
308
+ getNextState,
175
309
  reset,
176
310
  runNext,
177
- runUntil
311
+ runUntil,
312
+ runUntilNext
178
313
  };
179
314
  };
180
315
 
@@ -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
  };
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
+ * Valid states for the runner.
5
+ *
4
6
  * @module runner-states
5
7
  */
6
8
 
package/package.json CHANGED
@@ -1,14 +1,17 @@
1
1
  {
2
2
  "name": "pa11y-ci-reporter-runner",
3
- "version": "0.7.1",
3
+ "version": "2.0.0",
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.10.0",
40
- "jest": "^27.5.1",
41
- "jest-junit": "^13.0.0",
41
+ "@aarongoldenthal/eslint-config-standard": "^14.0.0",
42
+ "eslint": "^8.16.0",
43
+ "jest": "^28.1.0",
44
+ "jest-junit": "^13.2.0",
42
45
  "markdownlint-cli": "^0.31.1",
43
- "pa11y-ci-reporter-cli-summary": "^1.0.1"
46
+ "pa11y-ci-reporter-cli-summary": "^1.1.0",
47
+ "prettier": "^2.6.2"
44
48
  },
45
49
  "dependencies": {
46
50
  "lodash": "^4.17.21",
47
- "xstate": "^4.30.5"
51
+ "xstate": "^4.32.1"
48
52
  }
49
53
  }