testilo 37.0.0 → 38.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 +30 -15
- package/call.js +20 -15
- package/merge.js +11 -16
- package/package.json +1 -1
- package/procs/digest/tdp43e/index.js +4 -2
- package/procs/score/tic43.js +37 -20
- package/procs/score/tsp43.js +14 -2
- package/procs/util.js +6 -10
- package/script.js +207 -160
package/README.md
CHANGED
|
@@ -199,9 +199,22 @@ Here is a script:
|
|
|
199
199
|
isolate: true,
|
|
200
200
|
standard: 'also',
|
|
201
201
|
observe: false,
|
|
202
|
-
|
|
202
|
+
device: {
|
|
203
|
+
id: 'iPhone 8',
|
|
204
|
+
windowOptions: {
|
|
205
|
+
reducedMotion: 'no-preference',
|
|
206
|
+
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/17.4 Mobile/15A372 Safari/604.1',
|
|
207
|
+
viewport: {
|
|
208
|
+
width: 375,
|
|
209
|
+
height: 667
|
|
210
|
+
},
|
|
211
|
+
deviceScaleFactor: 2,
|
|
212
|
+
isMobile: true,
|
|
213
|
+
hasTouch: true,
|
|
214
|
+
defaultBrowserType: 'webkit'
|
|
215
|
+
}
|
|
216
|
+
},
|
|
203
217
|
browserID: 'webkit',
|
|
204
|
-
lowMotion: false,
|
|
205
218
|
timeLimit: 80,
|
|
206
219
|
creationTimeStamp: ''
|
|
207
220
|
executionTimeStamp: '',
|
|
@@ -249,9 +262,8 @@ A script has several properties that specify facts about the jobs to be created.
|
|
|
249
262
|
- `standard`: When Testaro performs a job, every tool produces its own report. Testaro can convert the test results of each tool report to standard results. The `standard` property specifies how to handle standardization. If `also`, Testaro will include in its reports both the original results of the tests of tools and the Testaro-standardized results. If `only`, reports will include only the standardized test results. If `no`, reports will include only the original results, without standardization.
|
|
250
263
|
- `observe`: Testaro jobs can allow granular observation. If `true`, the job will do so. If `false`, Testaro will not report job progress, but will send a report to the server only when the report is completed. It is generally user-friendly to allow granular observation, and for user applications to implement it, if they make users wait while jobs are assigned and performed, since that process typically takes a few minutes.
|
|
251
264
|
- `timeLimit`: This specifies the maximum duration, in seconds, of a job. Testaro will abort jobs that are not completed within that time.
|
|
252
|
-
- `
|
|
265
|
+
- `device`: This specifies the ID of a device and properties that each new browser context (window) will have that correspond to that device. The permitted devices are those (about 125 in number) [recognized by Playwright](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/deviceDescriptorsSource.json), as well as `'default'`.
|
|
253
266
|
- `browserID`: This specifies the default browser type (`'chromium'`, `'firefox'`, or `'webkit'`) of the job.
|
|
254
|
-
- `lowMotion`: This is true if the browser is to create tabs with the `reduce-motion` option set to `reduce` instead of `no-preference`. This makes the browser act as if the user has chosen a [motion-reduction option in the settings of the operating system or browser](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion#user_preferences). Motions on pages are, however, often immune to this setting.
|
|
255
267
|
- `creationTimeStamp` and `executionTimeStamp`: These properties will have values assigned to them when jobs are created from the script.
|
|
256
268
|
- `sendReportTo`: This is a URL that Testaro is to send its job reports to, or `''` if the jobs will not be network jobs.
|
|
257
269
|
- `target`: This object contains blank `url` and `what` properties, which will be populated each time the script is converted to a job.
|
|
@@ -269,9 +281,10 @@ You can use the `script()` function of the `script` module to simplify the creat
|
|
|
269
281
|
|
|
270
282
|
#### Without options
|
|
271
283
|
|
|
272
|
-
In its simplest form, `script()` requires
|
|
273
|
-
|
|
274
|
-
|
|
284
|
+
In its simplest form, `script()` requires 3 string arguments:
|
|
285
|
+
1. An ID for the script
|
|
286
|
+
1. A description of the script
|
|
287
|
+
1. A device ID
|
|
275
288
|
|
|
276
289
|
Called in this way, `script()` produces a script that tells Testaro to perform the tests for all of the evaluation rules defined by all of the tools integrated by Testaro.
|
|
277
290
|
|
|
@@ -320,16 +333,16 @@ A module can invoke `script()` in one of these ways:
|
|
|
320
333
|
|
|
321
334
|
```javaScript
|
|
322
335
|
const {script} = require('testilo/script');
|
|
323
|
-
const scriptObj = script('monthly', 'landmarks');
|
|
336
|
+
const scriptObj = script('monthly', 'landmarks', 'default');
|
|
324
337
|
```
|
|
325
338
|
|
|
326
339
|
```javaScript
|
|
327
340
|
const {script} = require('testilo/script');
|
|
328
341
|
const options = {…};
|
|
329
|
-
const scriptObj = script('monthly', 'landmarks', options);
|
|
342
|
+
const scriptObj = script('monthly', 'landmarks', 'default', options);
|
|
330
343
|
```
|
|
331
344
|
|
|
332
|
-
In this example, the script will have `'monthly'` as its ID and `'
|
|
345
|
+
In this example, the script will have `'monthly'` as its ID, `'landmarks'` as its description, and `'default'` as its device. It will tell Testaro to test for all evaluation rules if the first form is used, or for all rules specified by the options argument if the second form is used.
|
|
333
346
|
|
|
334
347
|
The invoking module can further modify and use the script (`scriptObj`) as needed.
|
|
335
348
|
|
|
@@ -338,9 +351,9 @@ The invoking module can further modify and use the script (`scriptObj`) as neede
|
|
|
338
351
|
A user can invoke `script()` by executing one of these statements in the Testilo project directory:
|
|
339
352
|
|
|
340
353
|
```javascript
|
|
341
|
-
node call script id what
|
|
342
|
-
node call script id what tools toolID0 toolID1 toolID2 …
|
|
343
|
-
node call script id what issues tic99 issueID0 issueID1 …
|
|
354
|
+
node call script id what deviceID
|
|
355
|
+
node call script id what deviceID tools toolID0 toolID1 toolID2 …
|
|
356
|
+
node call script id what deviceID issues tic99 issueID0 issueID1 …
|
|
344
357
|
```
|
|
345
358
|
|
|
346
359
|
The first form will create a script with no restrictions.
|
|
@@ -349,7 +362,7 @@ The second form will create a script that prescribes tests for all the rules of
|
|
|
349
362
|
|
|
350
363
|
The third form will create a script that prescribes tests for all the rules classified by the named issue-classification file into any of the specified issues.
|
|
351
364
|
|
|
352
|
-
In this statement, replace `id` with an ID for the script, such as `headings1`, consisting of ASCII alphanumeric characters, or with `''` if you want Testilo to create an ID. Replace `what` with a string describing the script, or with `''` if you want Testilo to create a generic description.
|
|
365
|
+
In this statement, replace `id` with an ID for the script, such as `headings1`, consisting of ASCII alphanumeric characters, or with `''` if you want Testilo to create an ID. Replace `what` with a string describing the script, or with `''` if you want Testilo to create a generic description. Replace `device` with a valid device ID.
|
|
353
366
|
|
|
354
367
|
The `call` module will retrieve the named classification, if any.
|
|
355
368
|
The `script` module will create a script.
|
|
@@ -362,7 +375,7 @@ When the `script` module creates a script for you, it does not ask you for all o
|
|
|
362
375
|
- `isolate`: `true`
|
|
363
376
|
- `standard`: `'only'`
|
|
364
377
|
- `observe`: `false`
|
|
365
|
-
- `
|
|
378
|
+
- `device.windowOptions.reduce-motion`: `'no-preference'`
|
|
366
379
|
- `browserID`: `'webkit'`
|
|
367
380
|
- `lowMotion`: `false`
|
|
368
381
|
- `timeLimit`: 50 plus 30 per tool
|
|
@@ -375,6 +388,8 @@ When the `script` module creates a script for you, it does not ask you for all o
|
|
|
375
388
|
- `testaro` test act: `withItems` = true, `stopOnFail` = `false`
|
|
376
389
|
- `wave` test act: `reportType` = 4
|
|
377
390
|
|
|
391
|
+
The `device.windowOptions` object has, in addition to `reducedMotion`, other properties shown in the above script example. The `script` module will set them according to the specified device. If you specify `'default'` as the device ID, the only property will be `reducedMotion`.
|
|
392
|
+
|
|
378
393
|
The `webkit` browser type is selected because the other browser types corrupt some tests. The `ibm` test is performed on the existing page content because some targets cause HTTP2 protocol errors when the `ibm` tool tries to visit them.
|
|
379
394
|
|
|
380
395
|
After you invoke `script`, you can edit the script that it creates to revise any of these options.
|
package/call.js
CHANGED
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
// Module to keep secrets.
|
|
40
40
|
require('dotenv').config();
|
|
41
41
|
// Module to perform common operations.
|
|
42
|
-
const {getFileID, getNowStamp, getRandomString} = require('./procs/util');
|
|
42
|
+
const {getFileID, getNowStamp, getRandomString, isToolID} = require('./procs/util');
|
|
43
43
|
// Function to process files.
|
|
44
44
|
const fs = require('fs/promises');
|
|
45
45
|
// Function to process a list-to-batch conversion.
|
|
46
46
|
const {batch} = require('./batch');
|
|
47
47
|
// Function to create a script from rule specifications.
|
|
48
|
-
const {script
|
|
48
|
+
const {script} = require('./script');
|
|
49
49
|
// Function to process a merger.
|
|
50
50
|
const {merge} = require('./merge');
|
|
51
51
|
// Function to score reports.
|
|
@@ -126,7 +126,7 @@ const callBatch = async (id, what) => {
|
|
|
126
126
|
}
|
|
127
127
|
};
|
|
128
128
|
// Fulfills a script-creation request.
|
|
129
|
-
const callScript = async (scriptID, what, optionType, ... optionDetails) => {
|
|
129
|
+
const callScript = async (scriptID, what, deviceID, optionType, ... optionDetails) => {
|
|
130
130
|
// Sanitize the script ID.
|
|
131
131
|
scriptID = scriptID.replace(/[^a-zA-Z0-9]/g, '');
|
|
132
132
|
if (scriptID === '') {
|
|
@@ -138,7 +138,7 @@ const callScript = async (scriptID, what, optionType, ... optionDetails) => {
|
|
|
138
138
|
if (optionType === 'tools') {
|
|
139
139
|
if (
|
|
140
140
|
optionDetails.length === new Set(optionDetails).size
|
|
141
|
-
&& optionDetails.every(toolID =>
|
|
141
|
+
&& optionDetails.every(toolID => isToolID(toolID))
|
|
142
142
|
) {
|
|
143
143
|
optionArg.type = 'tools';
|
|
144
144
|
optionArg.specs = optionDetails;
|
|
@@ -186,17 +186,22 @@ const callScript = async (scriptID, what, optionType, ... optionDetails) => {
|
|
|
186
186
|
return;
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
|
-
// Create a script with
|
|
190
|
-
const scriptObj = script(scriptID,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
189
|
+
// Create a script with specified device ID and default browser ID.
|
|
190
|
+
const scriptObj = script(scriptID, what, deviceID, optionArg);
|
|
191
|
+
if (scriptObj) {
|
|
192
|
+
try {
|
|
193
|
+
// Save it.
|
|
194
|
+
const scriptJSON = JSON.stringify(scriptObj, null, 2);
|
|
195
|
+
const scriptPath = `${specDir}/scripts/${scriptID}.json`;
|
|
196
|
+
await fs.writeFile(scriptPath, `${scriptJSON}\n`);
|
|
197
|
+
console.log(`Script created and saved as ${scriptPath}`);
|
|
198
|
+
}
|
|
199
|
+
catch(error) {
|
|
200
|
+
console.log(`ERROR saving script (${error.message})`);
|
|
201
|
+
}
|
|
197
202
|
}
|
|
198
|
-
|
|
199
|
-
console.log(
|
|
203
|
+
else {
|
|
204
|
+
console.log('ERROR: Script creation failed');
|
|
200
205
|
}
|
|
201
206
|
};
|
|
202
207
|
// Fulfills a merging request.
|
|
@@ -487,7 +492,7 @@ if (fn === 'batch' && fnArgs.length === 2) {
|
|
|
487
492
|
console.log('Execution completed');
|
|
488
493
|
});
|
|
489
494
|
}
|
|
490
|
-
else if (fn === 'script' && (fnArgs.length ===
|
|
495
|
+
else if (fn === 'script' && (fnArgs.length === 3 || fnArgs.length > 4)) {
|
|
491
496
|
callScript(... fnArgs)
|
|
492
497
|
.then(() => {
|
|
493
498
|
console.log('Execution completed');
|
package/merge.js
CHANGED
|
@@ -50,7 +50,7 @@ const mergeIDLength = 2;
|
|
|
50
50
|
|
|
51
51
|
// Merges a script and a batch and returns jobs.
|
|
52
52
|
exports.merge = (script, batch, executionTimeStamp) => {
|
|
53
|
-
// If
|
|
53
|
+
// If an execution time stamp was specified:
|
|
54
54
|
if (executionTimeStamp) {
|
|
55
55
|
// If it is invalid:
|
|
56
56
|
if (! dateOf(executionTimeStamp)) {
|
|
@@ -100,44 +100,39 @@ exports.merge = (script, batch, executionTimeStamp) => {
|
|
|
100
100
|
}
|
|
101
101
|
// Initialize an array of jobs.
|
|
102
102
|
const jobs = [];
|
|
103
|
-
// For each target in the batch:
|
|
104
103
|
const {targets} = batch;
|
|
105
104
|
const targetIDs = Object.keys(targets);
|
|
105
|
+
// For each target in the batch:
|
|
106
106
|
targetIDs.forEach((what, index) => {
|
|
107
|
-
// If the target has the required identifiers:
|
|
108
107
|
const {actGroups, url} = targets[what];
|
|
108
|
+
// If the target has the required identifiers:
|
|
109
109
|
if (actGroups && url) {
|
|
110
|
-
// Initialize a job.
|
|
110
|
+
// Initialize a job as a copy of the template.
|
|
111
111
|
const job = JSON.parse(JSON.stringify(protoJob));
|
|
112
112
|
const {sources, target} = job;
|
|
113
113
|
// Make the job ID unique.
|
|
114
114
|
const targetSuffix = alphaNumOf(index);
|
|
115
115
|
job.id = `${executionTimeStamp}-${sources.mergeID}-${targetSuffix}`;
|
|
116
|
-
// If the target is the last one:
|
|
117
|
-
if (index === targetIDs.length - 1) {
|
|
118
|
-
// Add that fact to the sources property of the job.
|
|
119
|
-
sources.lastTarget = true;
|
|
120
|
-
}
|
|
121
116
|
// Populate the target-specific properties of the job.
|
|
122
117
|
target.what = what;
|
|
123
118
|
target.url = url;
|
|
124
|
-
// Replace each placeholder
|
|
119
|
+
// Replace each placeholder in the job with a copy of the named act group of the target.
|
|
125
120
|
let {acts} = job;
|
|
126
121
|
for (const actIndex in acts) {
|
|
127
122
|
const act = acts[actIndex];
|
|
128
123
|
if (act.type === 'placeholder') {
|
|
129
124
|
const replacerName = act.which;
|
|
130
|
-
const replacerActs = actGroups[replacerName];
|
|
125
|
+
const replacerActs = JSON.parse(JSON.stringify(actGroups[replacerName]));
|
|
131
126
|
if (replacerName && actGroups && replacerActs) {
|
|
132
|
-
// Add properties to any launch act in the replacer.
|
|
127
|
+
// Add any override properties to any launch act in the replacer.
|
|
133
128
|
for (const replacerAct of replacerActs) {
|
|
134
129
|
if (replacerAct.type === 'launch') {
|
|
135
|
-
if (act.deviceID) {
|
|
136
|
-
replacerAct.deviceID = act.deviceID;
|
|
137
|
-
}
|
|
138
130
|
if (act.browserID) {
|
|
139
131
|
replacerAct.browserID = act.browserID;
|
|
140
132
|
}
|
|
133
|
+
if (act.target) {
|
|
134
|
+
replacerAct.target = act.target;
|
|
135
|
+
}
|
|
141
136
|
}
|
|
142
137
|
};
|
|
143
138
|
acts[actIndex] = replacerActs;
|
|
@@ -146,7 +141,7 @@ exports.merge = (script, batch, executionTimeStamp) => {
|
|
|
146
141
|
console.log(`ERROR: Placeholder for target ${what} not replaceable`);
|
|
147
142
|
}
|
|
148
143
|
}
|
|
149
|
-
}
|
|
144
|
+
};
|
|
150
145
|
// Flatten the acts.
|
|
151
146
|
job.acts = acts.flat();
|
|
152
147
|
// Append the job to the array of jobs.
|
package/package.json
CHANGED
|
@@ -57,7 +57,9 @@ const getIssueScoreRow = (issueConstants, issueDetails) => {
|
|
|
57
57
|
};
|
|
58
58
|
// Adds parameters to a query for a digest.
|
|
59
59
|
const populateQuery = (report, query) => {
|
|
60
|
-
const {
|
|
60
|
+
const {
|
|
61
|
+
browserID, device, id, isolate, lowMotion, score, sources, standard, strict, target
|
|
62
|
+
} = report;
|
|
61
63
|
const {agent, requester, script} = sources;
|
|
62
64
|
const {scoreProcID, summary, details} = score;
|
|
63
65
|
query.ts = script;
|
|
@@ -74,7 +76,7 @@ const populateQuery = (report, query) => {
|
|
|
74
76
|
= ['original', 'standard', 'original and standard'][['no', 'only', 'also'].indexOf(standard)];
|
|
75
77
|
query.motion = lowMotion ? 'requested' : 'not requested';
|
|
76
78
|
query.requester = requester;
|
|
77
|
-
query.device =
|
|
79
|
+
query.device = device.id;
|
|
78
80
|
query.browser = browserID;
|
|
79
81
|
query.agent = agent ? ` on agent ${agent}` : '';
|
|
80
82
|
query.reportURL = process.env.SCORED_REPORT_URL.replace('__id__', id);
|
package/procs/score/tic43.js
CHANGED
|
@@ -3130,16 +3130,6 @@ exports.issues = {
|
|
|
3130
3130
|
variable: false,
|
|
3131
3131
|
quality: 1,
|
|
3132
3132
|
what: 'Element has an event handler but no valid ARIA role'
|
|
3133
|
-
},
|
|
3134
|
-
table_aria_descendants: {
|
|
3135
|
-
variable: false,
|
|
3136
|
-
quality: 1,
|
|
3137
|
-
what: 'Table structure element specifies an explicit role within the table container'
|
|
3138
|
-
},
|
|
3139
|
-
aria_child_valid: {
|
|
3140
|
-
variable: false,
|
|
3141
|
-
quality: 1,
|
|
3142
|
-
what: 'Child element has a role not allowed for the role of the parent'
|
|
3143
3133
|
}
|
|
3144
3134
|
},
|
|
3145
3135
|
nuVal: {
|
|
@@ -3153,20 +3143,10 @@ exports.issues = {
|
|
|
3153
3143
|
quality: 1,
|
|
3154
3144
|
what: 'img element has a role attribute but no alt attribute'
|
|
3155
3145
|
},
|
|
3156
|
-
'A figure element with a figcaption descendant must not have a role attribute.': {
|
|
3157
|
-
variable: false,
|
|
3158
|
-
quality: 1,
|
|
3159
|
-
what: 'figure element has a figcaption descendant but has a role attribute'
|
|
3160
|
-
},
|
|
3161
3146
|
'^Discarding unrecognized token .+ from value of attribute role. Browsers ignore any token that is not a defined ARIA non-abstract role.*$': {
|
|
3162
3147
|
variable: true,
|
|
3163
3148
|
quality: 1,
|
|
3164
3149
|
what: 'Invalid role'
|
|
3165
|
-
},
|
|
3166
|
-
'^The role attribute must not be used on a .+ element which has a table ancestor with no role attribute, or with a role attribute whose value is table, grid, or treegrid.*$': {
|
|
3167
|
-
variable: true,
|
|
3168
|
-
quality: 1,
|
|
3169
|
-
what: 'Table cell has a role attribute'
|
|
3170
3150
|
}
|
|
3171
3151
|
},
|
|
3172
3152
|
qualWeb: {
|
|
@@ -3178,6 +3158,43 @@ exports.issues = {
|
|
|
3178
3158
|
}
|
|
3179
3159
|
}
|
|
3180
3160
|
},
|
|
3161
|
+
roleHierarchyBad: {
|
|
3162
|
+
summary: 'ancestor and descendant elements have incompatible roles',
|
|
3163
|
+
why: 'User may misunderstand or be blocked from exposure to an item',
|
|
3164
|
+
wcag: '4.1.2',
|
|
3165
|
+
weight: 4,
|
|
3166
|
+
tools: {
|
|
3167
|
+
ibm: {
|
|
3168
|
+
aria_child_valid: {
|
|
3169
|
+
variable: false,
|
|
3170
|
+
quality: 1,
|
|
3171
|
+
what: 'Child element has a role not allowed for the role of the parent'
|
|
3172
|
+
},
|
|
3173
|
+
aria_descendant_valid: {
|
|
3174
|
+
variable: false,
|
|
3175
|
+
quality: 1,
|
|
3176
|
+
what: 'Element and descendant roles make browsers ignore a descendant'
|
|
3177
|
+
},
|
|
3178
|
+
table_aria_descendants: {
|
|
3179
|
+
variable: false,
|
|
3180
|
+
quality: 1,
|
|
3181
|
+
what: 'Table structure element specifies an explicit role within the table container'
|
|
3182
|
+
}
|
|
3183
|
+
},
|
|
3184
|
+
nuVal: {
|
|
3185
|
+
'A figure element with a figcaption descendant must not have a role attribute.': {
|
|
3186
|
+
variable: false,
|
|
3187
|
+
quality: 1,
|
|
3188
|
+
what: 'figure element has a figcaption descendant but has a role attribute'
|
|
3189
|
+
},
|
|
3190
|
+
'^The role attribute must not be used on a .+ element which has a table ancestor with no role attribute, or with a role attribute whose value is table, grid, or treegrid.*$': {
|
|
3191
|
+
variable: true,
|
|
3192
|
+
quality: 1,
|
|
3193
|
+
what: 'Table cell has a role attribute'
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
},
|
|
3181
3198
|
roleRedundant: {
|
|
3182
3199
|
summary: 'role redundant',
|
|
3183
3200
|
why: 'Document includes unnecessary code',
|
package/procs/score/tsp43.js
CHANGED
|
@@ -323,8 +323,20 @@ exports.scorer = report => {
|
|
|
323
323
|
summary.tool = toolWeight * details.severity.total.reduce(
|
|
324
324
|
(total, current, index) => total + severityWeights[index] * current, 0
|
|
325
325
|
);
|
|
326
|
-
//
|
|
327
|
-
|
|
326
|
+
// Get the minimum count of violating elements.
|
|
327
|
+
const actRuleIDs = testActs.map(
|
|
328
|
+
act => act.standardResult.instances.map(instance => `${act.which}:${instance.ruleID}`)
|
|
329
|
+
);
|
|
330
|
+
const allRuleIDs = actRuleIDs.flat();
|
|
331
|
+
const ruleCounts = Array
|
|
332
|
+
.from(new Set(allRuleIDs))
|
|
333
|
+
.map(ruleID => allRuleIDs.filter(id => id === ruleID).length);
|
|
334
|
+
/*
|
|
335
|
+
Add the summary element total to the score, based on the count of identified violating
|
|
336
|
+
elements or the largest count of instances of violations of any rule, whichever is
|
|
337
|
+
greater.
|
|
338
|
+
*/
|
|
339
|
+
summary.element = elementWeight * Math.max(pathIDs.size, ... ruleCounts);
|
|
328
340
|
// Add the summary prevention total to the score.
|
|
329
341
|
summary.prevention = Object.values(details.prevention).reduce(
|
|
330
342
|
(total, current) => total + current, 0
|
package/procs/util.js
CHANGED
|
@@ -25,11 +25,6 @@
|
|
|
25
25
|
Utility functions.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
// IMPORTS
|
|
29
|
-
|
|
30
|
-
// Devices recognized by Playwright.
|
|
31
|
-
const {devices} = require('playwright');
|
|
32
|
-
|
|
33
28
|
// CONSTANTS
|
|
34
29
|
|
|
35
30
|
// Array of 62 alphanumeric characters.
|
|
@@ -39,6 +34,10 @@ const alphaNumChars = (() => {
|
|
|
39
34
|
const lowers = Array(26).fill('').map((letter, index) => String.fromCodePoint(97 + index));
|
|
40
35
|
return digits.concat(uppers, lowers);
|
|
41
36
|
})();
|
|
37
|
+
// Tools.
|
|
38
|
+
const toolIDs = exports.toolIDs = [
|
|
39
|
+
'alfa', 'aslint', 'axe', 'ed11y', 'htmlcs', 'ibm', 'nuVal', 'qualWeb', 'testaro', 'wave'
|
|
40
|
+
];
|
|
42
41
|
|
|
43
42
|
// FUNCTIONS
|
|
44
43
|
|
|
@@ -114,8 +113,5 @@ exports.getBarCell = (num, colMax, svgWidth, isRight = false) => {
|
|
|
114
113
|
const cell = `<td aria-hidden="true"${rightClass}>${svg}</td>`;
|
|
115
114
|
return cell;
|
|
116
115
|
};
|
|
117
|
-
// Returns whether a
|
|
118
|
-
exports.
|
|
119
|
-
const deviceIDs = Object.keys(devices);
|
|
120
|
-
return deviceIDs.concat('default').includes(deviceID);
|
|
121
|
-
};
|
|
116
|
+
// Returns whether a tool ID is the ID of a Testaro-integrated tool.
|
|
117
|
+
exports.isToolID = toolID => toolIDs.includes(toolID);
|
package/script.js
CHANGED
|
@@ -25,180 +25,227 @@
|
|
|
25
25
|
Creates and returns a script to perform the tests for issues.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// IMPORTS
|
|
29
29
|
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// Module to handle secrets.
|
|
31
|
+
require('dotenv').config();
|
|
32
|
+
// Devices recognized by Playwright.
|
|
33
|
+
const {devices} = require('playwright');
|
|
34
|
+
// Utility module.
|
|
35
|
+
const {isToolID, toolIDs} = require('./procs/util');
|
|
34
36
|
|
|
35
37
|
// ########## FUNCTIONS
|
|
36
38
|
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
specs.forEach(spec => {
|
|
52
|
-
toolsRulesData[spec] = [];
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
// Otherwise, if the option type is issues and is valid:
|
|
56
|
-
else if (
|
|
57
|
-
type === 'issues'
|
|
58
|
-
&& typeof specs === 'object'
|
|
59
|
-
&& specs.issues
|
|
60
|
-
&& specs.issueIDs
|
|
61
|
-
&& typeof specs.issues === 'object'
|
|
62
|
-
&& Array.isArray(specs.issueIDs)
|
|
63
|
-
&& specs.issueIDs.length
|
|
64
|
-
) {
|
|
65
|
-
// For each specified issue:
|
|
66
|
-
const {issueIDs, issues} = specs;
|
|
67
|
-
issueIDs.forEach(issueID => {
|
|
68
|
-
// If it exists in the classification:
|
|
69
|
-
const issueData = issues[issueID];
|
|
70
|
-
if (issueData) {
|
|
71
|
-
// For each tool that tests for the issue:
|
|
72
|
-
const issueToolIDs = Object.keys(issueData.tools);
|
|
73
|
-
issueToolIDs.forEach(issueToolID => {
|
|
74
|
-
// For each of the rules of the tool for the issue:
|
|
75
|
-
toolsRulesData[issueToolID] ??= [];
|
|
76
|
-
const toolRuleIDs = toolsRulesData[issueToolID];
|
|
77
|
-
const toolData = issueData.tools[issueToolID];
|
|
78
|
-
Object.keys(toolData).forEach(ruleID => {
|
|
79
|
-
// Add the rule to the data on tools and rules.
|
|
80
|
-
let rulePrefix = '';
|
|
81
|
-
if (issueToolID === 'nuVal') {
|
|
82
|
-
rulePrefix = toolData[ruleID].variable ? '~' : '=';
|
|
83
|
-
}
|
|
84
|
-
const fullRuleID = `${rulePrefix}${ruleID}`;
|
|
85
|
-
if (! toolRuleIDs.includes(fullRuleID)) {
|
|
86
|
-
toolRuleIDs.push(fullRuleID);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
// Otherwise, i.e. if it does not exist in the classification:
|
|
92
|
-
else {
|
|
93
|
-
// Report this and quit.
|
|
94
|
-
console.log(`ERROR: Issue ${issueID} not in issue classification`);
|
|
95
|
-
return {};
|
|
96
|
-
}
|
|
97
|
-
});
|
|
39
|
+
// Returns whether a device ID is recognized by Playwright.
|
|
40
|
+
const isDeviceID = deviceID => deviceID === 'default' || !! devices[deviceID];
|
|
41
|
+
// Returns options for a new browser context (window).
|
|
42
|
+
const getWindowOptions = deviceID => {
|
|
43
|
+
// If the argument is valid:
|
|
44
|
+
if (isDeviceID(deviceID)) {
|
|
45
|
+
// Set the reducedMotion option.
|
|
46
|
+
const options = {
|
|
47
|
+
reducedMotion: 'no-preference'
|
|
48
|
+
};
|
|
49
|
+
// If the default device was specified:
|
|
50
|
+
if (deviceID === 'default') {
|
|
51
|
+
// Return the set option.
|
|
52
|
+
return options;
|
|
98
53
|
}
|
|
99
|
-
// Otherwise, i.e. if
|
|
54
|
+
// Otherwise, i.e. if a non-default device was specified:
|
|
100
55
|
else {
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
56
|
+
// Get its properties.
|
|
57
|
+
const deviceProperties = devices[deviceID];
|
|
58
|
+
// Return the reduce-motion and device options.
|
|
59
|
+
return {
|
|
60
|
+
... options,
|
|
61
|
+
... deviceProperties
|
|
62
|
+
};
|
|
104
63
|
}
|
|
105
64
|
}
|
|
106
|
-
// Otherwise, i.e. if
|
|
65
|
+
// Otherwise, i.e. if the arguments are not valid:
|
|
107
66
|
else {
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
toolsRulesData[toolID] = [];
|
|
111
|
-
});
|
|
67
|
+
// Return this.
|
|
68
|
+
return null;
|
|
112
69
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
script: id,
|
|
134
|
-
batch: '',
|
|
135
|
-
mergeID: '',
|
|
136
|
-
requester: process.env.REQUESTER || ''
|
|
137
|
-
},
|
|
138
|
-
acts: [
|
|
139
|
-
{
|
|
140
|
-
type: 'placeholder',
|
|
141
|
-
which: 'main'
|
|
70
|
+
};
|
|
71
|
+
// Creates and returns a script.
|
|
72
|
+
exports.script = (id, what, deviceID, options = {}) => {
|
|
73
|
+
// If the arguments are valid:
|
|
74
|
+
if (id && what && isDeviceID(deviceID)) {
|
|
75
|
+
const toolsRulesData = {};
|
|
76
|
+
// If options are specified:
|
|
77
|
+
const {type, specs} = options;
|
|
78
|
+
if (type && specs) {
|
|
79
|
+
// If the option type is tools and is valid:
|
|
80
|
+
if (
|
|
81
|
+
type === 'tools'
|
|
82
|
+
&& Array.isArray(specs)
|
|
83
|
+
&& specs.length
|
|
84
|
+
&& specs.every(spec => isToolID(spec))
|
|
85
|
+
) {
|
|
86
|
+
// Initialize the data on tools and rules.
|
|
87
|
+
specs.forEach(spec => {
|
|
88
|
+
toolsRulesData[spec] = [];
|
|
89
|
+
});
|
|
142
90
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
91
|
+
// Otherwise, if the option type is issues and is valid:
|
|
92
|
+
else if (
|
|
93
|
+
type === 'issues'
|
|
94
|
+
&& typeof specs === 'object'
|
|
95
|
+
&& specs.issues
|
|
96
|
+
&& specs.issueIDs
|
|
97
|
+
&& typeof specs.issues === 'object'
|
|
98
|
+
&& Array.isArray(specs.issueIDs)
|
|
99
|
+
&& specs.issueIDs.length
|
|
100
|
+
) {
|
|
101
|
+
const {issueIDs, issues} = specs;
|
|
102
|
+
// For each specified issue:
|
|
103
|
+
issueIDs.forEach(issueID => {
|
|
104
|
+
const issueData = issues[issueID];
|
|
105
|
+
// If it exists in the classification:
|
|
106
|
+
if (issueData) {
|
|
107
|
+
const issueToolIDs = Object.keys(issueData.tools);
|
|
108
|
+
// For each tool that tests for the issue:
|
|
109
|
+
issueToolIDs.forEach(issueToolID => {
|
|
110
|
+
toolsRulesData[issueToolID] ??= [];
|
|
111
|
+
const toolRuleIDs = toolsRulesData[issueToolID];
|
|
112
|
+
const toolData = issueData.tools[issueToolID];
|
|
113
|
+
// For each of the rules of the tool for the issue:
|
|
114
|
+
Object.keys(toolData).forEach(ruleID => {
|
|
115
|
+
// Add the rule to the data on tools and rules.
|
|
116
|
+
let rulePrefix = '';
|
|
117
|
+
if (issueToolID === 'nuVal') {
|
|
118
|
+
rulePrefix = toolData[ruleID].variable ? '~' : '=';
|
|
119
|
+
}
|
|
120
|
+
const fullRuleID = `${rulePrefix}${ruleID}`;
|
|
121
|
+
if (! toolRuleIDs.includes(fullRuleID)) {
|
|
122
|
+
toolRuleIDs.push(fullRuleID);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// Otherwise, i.e. if it does not exist in the classification:
|
|
128
|
+
else {
|
|
129
|
+
// Report this and quit.
|
|
130
|
+
console.log(`ERROR: Issue ${issueID} not in issue classification`);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
171
133
|
});
|
|
172
|
-
// Replace the generic rule list with the QualWeb-format list.
|
|
173
|
-
toolAct.rules = specs;
|
|
174
134
|
}
|
|
175
|
-
// Otherwise, if the
|
|
176
|
-
else
|
|
177
|
-
//
|
|
178
|
-
|
|
135
|
+
// Otherwise, i.e. if the option specification is invalid:
|
|
136
|
+
else {
|
|
137
|
+
// Report this and quit.
|
|
138
|
+
console.log(`ERROR: Options invalid`);
|
|
139
|
+
return null;
|
|
179
140
|
}
|
|
180
141
|
}
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
toolAct.withNewContent = false;
|
|
188
|
-
}
|
|
189
|
-
else if (toolID === 'qualWeb') {
|
|
190
|
-
toolAct.withNewContent = false;
|
|
191
|
-
}
|
|
192
|
-
else if (toolID === 'testaro') {
|
|
193
|
-
toolAct.withItems = true;
|
|
194
|
-
toolAct.stopOnFail = false;
|
|
195
|
-
}
|
|
196
|
-
else if (toolID === 'wave') {
|
|
197
|
-
toolAct.reportType = 4;
|
|
142
|
+
// Otherwise, i.e. if options are not specified:
|
|
143
|
+
else {
|
|
144
|
+
// Populate the data on tools and rules.
|
|
145
|
+
toolIDs.forEach(toolID => {
|
|
146
|
+
toolsRulesData[toolID] = [];
|
|
147
|
+
});
|
|
198
148
|
}
|
|
199
|
-
//
|
|
200
|
-
scriptObj
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
149
|
+
// Initialize a script.
|
|
150
|
+
const scriptObj = {
|
|
151
|
+
id,
|
|
152
|
+
what,
|
|
153
|
+
strict: false,
|
|
154
|
+
isolate: true,
|
|
155
|
+
standard: 'only',
|
|
156
|
+
observe: false,
|
|
157
|
+
device: {
|
|
158
|
+
id: deviceID,
|
|
159
|
+
windowOptions: {}
|
|
160
|
+
},
|
|
161
|
+
browserID: 'webkit',
|
|
162
|
+
timeLimit: Math.round(50 + 30 * Object.keys(toolsRulesData).length),
|
|
163
|
+
creationTimeStamp: '',
|
|
164
|
+
executionTimeStamp: '',
|
|
165
|
+
sendReportTo: process.env.SEND_REPORT_TO || '',
|
|
166
|
+
target: {
|
|
167
|
+
what: '',
|
|
168
|
+
url: ''
|
|
169
|
+
},
|
|
170
|
+
sources: {
|
|
171
|
+
script: id,
|
|
172
|
+
batch: '',
|
|
173
|
+
mergeID: '',
|
|
174
|
+
requester: process.env.REQUESTER || ''
|
|
175
|
+
},
|
|
176
|
+
acts: [
|
|
177
|
+
{
|
|
178
|
+
type: 'placeholder',
|
|
179
|
+
which: 'main'
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
// Add the window options to the script.
|
|
184
|
+
scriptObj.device.windowOptions = getWindowOptions(deviceID);
|
|
185
|
+
// For each tool used:
|
|
186
|
+
Object.keys(toolsRulesData).forEach(toolID => {
|
|
187
|
+
// Initialize a test act for it.
|
|
188
|
+
const toolAct = {
|
|
189
|
+
type: 'test',
|
|
190
|
+
which: toolID
|
|
191
|
+
};
|
|
192
|
+
// If rules were specified:
|
|
193
|
+
const ruleIDs = toolsRulesData[toolID];
|
|
194
|
+
if (ruleIDs.length) {
|
|
195
|
+
// Add a rules array as a property to the act.
|
|
196
|
+
toolAct.rules = ruleIDs;
|
|
197
|
+
// If the tool is QualWeb:
|
|
198
|
+
if (toolID === 'qualWeb') {
|
|
199
|
+
// For each QualWeb module:
|
|
200
|
+
const specs = [];
|
|
201
|
+
const prefixes = {
|
|
202
|
+
act: 'QW-ACT-R',
|
|
203
|
+
wcag: 'QW-WCAG-T',
|
|
204
|
+
best: 'QW-BP'
|
|
205
|
+
};
|
|
206
|
+
Object.keys(prefixes).forEach(prefix => {
|
|
207
|
+
// Specify the rules of that module to be tested for.
|
|
208
|
+
const ids = toolAct.rules.filter(id => id.startsWith(prefixes[prefix]));
|
|
209
|
+
const integers = ids.map(id => id.slice(prefixes[prefix].length));
|
|
210
|
+
specs.push(`${prefix}:${integers.join(',')}`);
|
|
211
|
+
});
|
|
212
|
+
// Replace the generic rule list with the QualWeb-format list.
|
|
213
|
+
toolAct.rules = specs;
|
|
214
|
+
}
|
|
215
|
+
// Otherwise, if the tool is Testaro:
|
|
216
|
+
else if (toolID === 'testaro') {
|
|
217
|
+
// Prepend the inclusion option to the rule array.
|
|
218
|
+
toolAct.rules.unshift('y');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Add any needed option defaults to the act.
|
|
222
|
+
if (toolID === 'axe') {
|
|
223
|
+
toolAct.detailLevel = 2;
|
|
224
|
+
}
|
|
225
|
+
else if (toolID === 'ibm') {
|
|
226
|
+
toolAct.withItems = true;
|
|
227
|
+
toolAct.withNewContent = false;
|
|
228
|
+
}
|
|
229
|
+
else if (toolID === 'qualWeb') {
|
|
230
|
+
toolAct.withNewContent = false;
|
|
231
|
+
}
|
|
232
|
+
else if (toolID === 'testaro') {
|
|
233
|
+
toolAct.withItems = true;
|
|
234
|
+
toolAct.stopOnFail = false;
|
|
235
|
+
}
|
|
236
|
+
else if (toolID === 'wave') {
|
|
237
|
+
toolAct.reportType = 4;
|
|
238
|
+
}
|
|
239
|
+
// Add the act to the script.
|
|
240
|
+
scriptObj.acts.push(toolAct);
|
|
241
|
+
});
|
|
242
|
+
// Return the script.
|
|
243
|
+
return scriptObj;
|
|
244
|
+
}
|
|
245
|
+
// Otherwise, i.e. if the arguments are not valid:
|
|
246
|
+
else {
|
|
247
|
+
// Report this and quit.
|
|
248
|
+
console.log(`ERROR: Arguments invalid`);
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
204
251
|
}
|