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 +50 -22
- package/index.js +100 -22
- package/lib/config.js +3 -1
- package/lib/formatter.js +10 -5
- package/lib/pa11yci-machine.js +10 -2
- package/lib/pa11yci-service.js +205 -70
- package/lib/reporter-builder.js +3 -5
- package/lib/runner-states.js +2 -0
- package/package.json +16 -12
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
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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(
|
|
74
|
+
const { createRunner, RunnerStates } = require('pa11y-ci-reporter-runner');
|
|
64
75
|
|
|
65
|
-
const resultsFileName =
|
|
66
|
-
const reporterName =
|
|
67
|
-
const reporterOptions = {
|
|
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
|
-
|
|
74
|
-
|
|
84
|
+
'http://localhost:8080/page1-with-errors.html',
|
|
85
|
+
'http://localhost:8080/page1-no-errors.html',
|
|
75
86
|
{
|
|
76
|
-
url:
|
|
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(
|
|
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(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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 (
|
|
81
|
-
|
|
82
|
-
|
|
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 =>
|
|
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 = (
|
|
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(
|
|
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(
|
|
202
|
+
const service = serviceFactory(
|
|
203
|
+
Object.keys(pa11yciResults.results),
|
|
204
|
+
actions
|
|
205
|
+
);
|
|
171
206
|
|
|
172
207
|
/**
|
|
173
|
-
* Resets the runner to the initial
|
|
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
|
|
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
|
-
* @
|
|
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(
|
|
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 =
|
|
10
|
-
|
|
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)
|
|
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 =
|
|
66
|
+
module.exports.getPa11yResultsFromPa11yCiResults =
|
|
67
|
+
getPa11yResultsFromPa11yCiResults;
|
package/lib/pa11yci-machine.js
CHANGED
|
@@ -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
|
);
|
package/lib/pa11yci-service.js
CHANGED
|
@@ -1,11 +1,43 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
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 {
|
|
26
|
-
* @returns {boolean}
|
|
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 =>
|
|
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
|
|
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
|
|
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}
|
|
48
|
-
* @param {
|
|
49
|
-
* @param {string}
|
|
50
|
-
* @returns {boolean}
|
|
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 = (
|
|
53
|
-
return
|
|
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
|
-
*
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
*
|
|
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(
|
|
143
|
+
throw new Error(
|
|
144
|
+
'runner cannot accept a command while another command is pending, await previous command'
|
|
145
|
+
);
|
|
103
146
|
}
|
|
104
|
-
if (
|
|
105
|
-
throw new Error(
|
|
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-
|
|
111
|
-
* the
|
|
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 {
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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(
|
|
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(
|
|
210
|
+
await sendEvent(MachineEvents.NEXT);
|
|
145
211
|
};
|
|
146
212
|
|
|
147
213
|
/**
|
|
148
|
-
*
|
|
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
|
-
* @
|
|
153
|
-
* @
|
|
154
|
-
* @
|
|
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
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
250
|
+
if (!isAtTarget(getState(stateType), targetState, targetUrl)) {
|
|
169
251
|
const urlString = targetUrl ? ` for targetUrl "${targetUrl}"` : '';
|
|
170
|
-
throw new Error(
|
|
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
|
|
package/lib/reporter-builder.js
CHANGED
|
@@ -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/lib/runner-states.js
CHANGED
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pa11y-ci-reporter-runner",
|
|
3
|
-
"version": "0.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
11
|
-
"
|
|
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": "^
|
|
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
|
|
39
|
+
"homepage": "https://gitlab.com/gitlab-ci-utils/pa11y-ci-reporter-runner",
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@aarongoldenthal/eslint-config-standard": "^
|
|
39
|
-
"eslint": "^8.
|
|
40
|
-
"jest": "^
|
|
41
|
-
"jest-junit": "^13.
|
|
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
|
|
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.
|
|
51
|
+
"xstate": "^4.32.1"
|
|
48
52
|
}
|
|
49
53
|
}
|