testaro 40.0.2 → 41.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +213 -217
- package/actSpecs.js +3 -3
- package/dirWatch.js +2 -7
- package/netWatch.js +71 -78
- package/package.json +1 -1
- package/procs/dateOf.js +47 -0
- package/procs/device.js +59 -0
- package/procs/job.js +268 -0
- package/procs/standardize.js +16 -8
- package/run.js +815 -1068
- package/tests/ed11y.js +2 -2
package/run.js
CHANGED
|
@@ -25,20 +25,22 @@
|
|
|
25
25
|
Testaro main utility module.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// IMPORTS
|
|
29
29
|
|
|
30
30
|
// Module to keep secrets.
|
|
31
31
|
require('dotenv').config();
|
|
32
|
-
//
|
|
33
|
-
const {
|
|
32
|
+
// Module to validate jobs.
|
|
33
|
+
const {isValidJob, tools} = require('./procs/job');
|
|
34
34
|
// Module to standardize report formats.
|
|
35
35
|
const {standardize} = require('./procs/standardize');
|
|
36
36
|
// Module to identify element bounding boxes.
|
|
37
37
|
const {identify} = require('./procs/identify');
|
|
38
38
|
// Module to send a notice to an observer.
|
|
39
39
|
const {tellServer} = require('./procs/tellServer');
|
|
40
|
+
// Module to get device options.
|
|
41
|
+
const {getDeviceOptions, isDeviceID} = require('./procs/device');
|
|
40
42
|
|
|
41
|
-
//
|
|
43
|
+
// CONSTANTS
|
|
42
44
|
|
|
43
45
|
// Set DEBUG environment variable to 'true' to add debugging features.
|
|
44
46
|
const debug = process.env.DEBUG === 'true';
|
|
@@ -55,19 +57,6 @@ const moves = {
|
|
|
55
57
|
select: 'select',
|
|
56
58
|
text: 'input'
|
|
57
59
|
};
|
|
58
|
-
// Names and descriptions of tools.
|
|
59
|
-
const tools = {
|
|
60
|
-
alfa: 'alfa',
|
|
61
|
-
aslint: 'ASLint',
|
|
62
|
-
axe: 'Axe',
|
|
63
|
-
ed11y: 'Editoria11y',
|
|
64
|
-
htmlcs: 'HTML CodeSniffer WCAG 2.1 AA ruleset',
|
|
65
|
-
ibm: 'IBM Accessibility Checker',
|
|
66
|
-
nuVal: 'Nu Html Checker',
|
|
67
|
-
qualWeb: 'QualWeb',
|
|
68
|
-
testaro: 'Testaro',
|
|
69
|
-
wave: 'WAVE',
|
|
70
|
-
};
|
|
71
60
|
// Strings in log messages indicating errors.
|
|
72
61
|
const errorWords = [
|
|
73
62
|
'but not used',
|
|
@@ -100,219 +89,7 @@ let browserContext;
|
|
|
100
89
|
let currentPage;
|
|
101
90
|
let requestedURL = '';
|
|
102
91
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
// Validates a browser type.
|
|
106
|
-
const isBrowserType = type => ['chromium', 'firefox', 'webkit'].includes(type);
|
|
107
|
-
// Validates a load state.
|
|
108
|
-
const isState = string => ['loaded', 'idle'].includes(string);
|
|
109
|
-
// Validates a URL.
|
|
110
|
-
const isURL = string => /^(?:https?|file):\/\/[^\s]+$/.test(string);
|
|
111
|
-
// Validates a focusable tag name.
|
|
112
|
-
const isFocusable = string => ['a', 'button', 'input', 'select'].includes(string);
|
|
113
|
-
// Returns whether all elements of an array are numbers.
|
|
114
|
-
const areNumbers = array => array.every(element => typeof element === 'number');
|
|
115
|
-
// Returns whether all elements of an array are strings.
|
|
116
|
-
const areStrings = array => array.every(element => typeof element === 'string');
|
|
117
|
-
// Returns whether all properties of an object have array values.
|
|
118
|
-
const areArrays = object => Object.values(object).every(value => Array.isArray(value));
|
|
119
|
-
// Returns whether a variable has a specified type.
|
|
120
|
-
const hasType = (variable, type) => {
|
|
121
|
-
if (type === 'string') {
|
|
122
|
-
return typeof variable === 'string';
|
|
123
|
-
}
|
|
124
|
-
else if (type === 'array') {
|
|
125
|
-
return Array.isArray(variable);
|
|
126
|
-
}
|
|
127
|
-
else if (type === 'boolean') {
|
|
128
|
-
return typeof variable === 'boolean';
|
|
129
|
-
}
|
|
130
|
-
else if (type === 'number') {
|
|
131
|
-
return typeof variable === 'number';
|
|
132
|
-
}
|
|
133
|
-
else if (type === 'object') {
|
|
134
|
-
return typeof variable === 'object' && ! Array.isArray(variable);
|
|
135
|
-
}
|
|
136
|
-
else {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
// Returns whether a variable has a specified subtype.
|
|
141
|
-
const hasSubtype = (variable, subtype) => {
|
|
142
|
-
if (subtype) {
|
|
143
|
-
if (subtype === 'hasLength') {
|
|
144
|
-
return variable.length > 0;
|
|
145
|
-
}
|
|
146
|
-
else if (subtype === 'isURL') {
|
|
147
|
-
return isURL(variable);
|
|
148
|
-
}
|
|
149
|
-
else if (subtype === 'isBrowserType') {
|
|
150
|
-
return isBrowserType(variable);
|
|
151
|
-
}
|
|
152
|
-
else if (subtype === 'isFocusable') {
|
|
153
|
-
return isFocusable(variable);
|
|
154
|
-
}
|
|
155
|
-
else if (subtype === 'isTest') {
|
|
156
|
-
return tools[variable];
|
|
157
|
-
}
|
|
158
|
-
else if (subtype === 'isWaitable') {
|
|
159
|
-
return ['url', 'title', 'body'].includes(variable);
|
|
160
|
-
}
|
|
161
|
-
else if (subtype === 'areNumbers') {
|
|
162
|
-
return areNumbers(variable);
|
|
163
|
-
}
|
|
164
|
-
else if (subtype === 'areStrings') {
|
|
165
|
-
return areStrings(variable);
|
|
166
|
-
}
|
|
167
|
-
else if (subtype === 'areArrays') {
|
|
168
|
-
return areArrays(variable);
|
|
169
|
-
}
|
|
170
|
-
else if (subtype === 'isState') {
|
|
171
|
-
return isState(variable);
|
|
172
|
-
}
|
|
173
|
-
else {
|
|
174
|
-
console.log(`ERROR: ${subtype} not a known subtype`);
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
// Validates an act.
|
|
183
|
-
const isValidAct = act => {
|
|
184
|
-
// Identify the type of the act.
|
|
185
|
-
const type = act.type;
|
|
186
|
-
// If the type exists and is known:
|
|
187
|
-
if (type && actSpecs.etc[type]) {
|
|
188
|
-
// Copy the validator of the type for possible expansion.
|
|
189
|
-
const validator = Object.assign({}, actSpecs.etc[type][1]);
|
|
190
|
-
// If the type is test:
|
|
191
|
-
if (type === 'test') {
|
|
192
|
-
// Identify the test.
|
|
193
|
-
const toolName = act.which;
|
|
194
|
-
// If one was specified and is known:
|
|
195
|
-
if (toolName && tools[toolName]) {
|
|
196
|
-
// If it has special properties:
|
|
197
|
-
if (actSpecs.tools[toolName]) {
|
|
198
|
-
// Expand the validator by adding them.
|
|
199
|
-
Object.assign(validator, actSpecs.tools[toolName][1]);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// Otherwise, i.e. if no or an unknown test was specified:
|
|
203
|
-
else {
|
|
204
|
-
// Return invalidity.
|
|
205
|
-
return false;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
// Return whether the act is valid.
|
|
209
|
-
return Object.keys(validator).every(property => {
|
|
210
|
-
if (property === 'name') {
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
const vP = validator[property];
|
|
215
|
-
const aP = act[property];
|
|
216
|
-
// If it is optional and omitted or is present and valid:
|
|
217
|
-
const optAndNone = ! vP[0] && ! aP;
|
|
218
|
-
const isValidAct = aP !== undefined && hasType(aP, vP[1]) && hasSubtype(aP, vP[2]);
|
|
219
|
-
return optAndNone || isValidAct;
|
|
220
|
-
}
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
// Otherwise, i.e. if the act has an unknown or no type:
|
|
224
|
-
else {
|
|
225
|
-
// Return invalidity.
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
};
|
|
229
|
-
// Inserts a character periodically in a string.
|
|
230
|
-
const punctuate = (string, insertion, chunkSize) => {
|
|
231
|
-
const segments = [];
|
|
232
|
-
let startIndex = 0;
|
|
233
|
-
while (startIndex < string.length) {
|
|
234
|
-
segments.push(string.slice(startIndex, startIndex + chunkSize));
|
|
235
|
-
startIndex += chunkSize;
|
|
236
|
-
}
|
|
237
|
-
return segments.join(insertion);
|
|
238
|
-
};
|
|
239
|
-
// Converts a compact timestamp to a date.
|
|
240
|
-
const dateOf = timeStamp => {
|
|
241
|
-
if (/^\d{6}T\d{4}$/.test(timeStamp)) {
|
|
242
|
-
const dateString = punctuate(timeStamp.slice(0, 6), '-', 2);
|
|
243
|
-
const timeString = punctuate(timeStamp.slice(7, 11), ':', 2);
|
|
244
|
-
return new Date(`20${dateString}T${timeString}Z`);
|
|
245
|
-
} else {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
// Validates a report object.
|
|
250
|
-
const isValidReport = report => {
|
|
251
|
-
if (report) {
|
|
252
|
-
// Return whether the report is valid.
|
|
253
|
-
const {id, what, strict, timeLimit, acts, sources, creationTimeStamp, timeStamp} = report;
|
|
254
|
-
if (! id || typeof id !== 'string') {
|
|
255
|
-
return 'Bad report ID';
|
|
256
|
-
}
|
|
257
|
-
if (! what || typeof what !== 'string') {
|
|
258
|
-
return 'Bad report what';
|
|
259
|
-
}
|
|
260
|
-
if (typeof strict !== 'boolean') {
|
|
261
|
-
return 'Bad report strict';
|
|
262
|
-
}
|
|
263
|
-
if (typeof timeLimit !== 'number' || timeLimit < 1) {
|
|
264
|
-
return 'Bad report time limit';
|
|
265
|
-
}
|
|
266
|
-
if (! acts || ! Array.isArray(acts) || ! acts.length) {
|
|
267
|
-
return 'Bad report acts';
|
|
268
|
-
}
|
|
269
|
-
if (! acts.every(act => act.type && typeof act.type === 'string')) {
|
|
270
|
-
return 'Act with no type';
|
|
271
|
-
}
|
|
272
|
-
if (acts[0].type !== 'launch') {
|
|
273
|
-
return 'First act type not launch';
|
|
274
|
-
}
|
|
275
|
-
if (! ['chromium', 'webkit', 'firefox'].includes(acts[0].which)) {
|
|
276
|
-
return 'Bad first act which';
|
|
277
|
-
}
|
|
278
|
-
if (acts[0].type !== 'launch' || (
|
|
279
|
-
(
|
|
280
|
-
! acts[0].url
|
|
281
|
-
|| typeof acts[0].url !== 'string'
|
|
282
|
-
|| ! isURL(acts[0].url)
|
|
283
|
-
)
|
|
284
|
-
&& (
|
|
285
|
-
acts[1].type !== 'url'
|
|
286
|
-
|| ! acts[1].which
|
|
287
|
-
|| typeof acts[1].which !== 'string'
|
|
288
|
-
|| ! isURL(acts[1].which)
|
|
289
|
-
)
|
|
290
|
-
)) {
|
|
291
|
-
return 'First or second act has no valid URL';
|
|
292
|
-
}
|
|
293
|
-
const invalidAct = acts.find(act => ! isValidAct(act));
|
|
294
|
-
if (invalidAct) {
|
|
295
|
-
return `Invalid act:\n${JSON.stringify(invalidAct, null, 2)}`;
|
|
296
|
-
}
|
|
297
|
-
if (! sources || typeof sources !== 'object') {
|
|
298
|
-
return 'Bad report sources';
|
|
299
|
-
}
|
|
300
|
-
if (
|
|
301
|
-
! (creationTimeStamp && typeof creationTimeStamp === 'string' && dateOf(creationTimeStamp))
|
|
302
|
-
) {
|
|
303
|
-
return 'bad job creation time stamp';
|
|
304
|
-
}
|
|
305
|
-
if (! (timeStamp && typeof timeStamp === 'string')) {
|
|
306
|
-
return 'bad report time stamp';
|
|
307
|
-
}
|
|
308
|
-
return '';
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
return 'no report';
|
|
312
|
-
}
|
|
313
|
-
};
|
|
314
|
-
|
|
315
|
-
// ########## OTHER FUNCTIONS
|
|
92
|
+
// FUNCTIONS
|
|
316
93
|
|
|
317
94
|
// Returns a string with any final slash removed.
|
|
318
95
|
const deSlash = string => string.endsWith('/') ? string.slice(0, -1) : string;
|
|
@@ -409,27 +186,28 @@ const browserClose = async () => {
|
|
|
409
186
|
}
|
|
410
187
|
};
|
|
411
188
|
// Launches a browser, navigates to a URL, and returns browser data.
|
|
412
|
-
const launch = async (
|
|
413
|
-
|
|
414
|
-
|
|
189
|
+
const launch = async (report, url, debug, waits, deviceID, browserID, lowMotion) => {
|
|
190
|
+
// Get the default arguments.
|
|
191
|
+
url ??= report.url;
|
|
192
|
+
deviceID ??= report.deviceID;
|
|
193
|
+
browserID ??= report.browserID;
|
|
194
|
+
lowMotion ??= report.lowMotion;
|
|
415
195
|
// If the specified browser type exists:
|
|
416
|
-
|
|
417
|
-
|
|
196
|
+
if (! browserID || ['chromium', 'firefox', 'webkit'].includes(browserID)) {
|
|
197
|
+
// Create a browser of the specified or default type.
|
|
198
|
+
const browserType = require('playwright')[browserID || report.browserID];
|
|
418
199
|
// Close the current browser, if any.
|
|
419
200
|
await browserClose();
|
|
420
|
-
//
|
|
201
|
+
// Define browser options.
|
|
421
202
|
const browserOptions = {
|
|
422
203
|
logger: {
|
|
423
204
|
isEnabled: () => false,
|
|
424
205
|
log: (name, severity, message) => console.log(message.slice(0, 100))
|
|
425
206
|
}
|
|
426
207
|
};
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (waits) {
|
|
431
|
-
browserOptions.slowMo = waits;
|
|
432
|
-
}
|
|
208
|
+
browserOptions.headless = ! debug;
|
|
209
|
+
browserOptions.slowMo = waits || 0;
|
|
210
|
+
// Launch the browser.
|
|
433
211
|
browser = await browserType.launch(browserOptions)
|
|
434
212
|
// If the launch failed:
|
|
435
213
|
.catch(async error => {
|
|
@@ -440,135 +218,132 @@ const launch = async (
|
|
|
440
218
|
error: 'Browser launch failed'
|
|
441
219
|
};
|
|
442
220
|
});
|
|
443
|
-
//
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
// Log a summary of the message on the console.
|
|
492
|
-
const parts = [msgText.slice(0, 75)];
|
|
493
|
-
if (msgText.length > 75) {
|
|
494
|
-
parts.push(msgText.slice(75, 150));
|
|
495
|
-
if (msgText.length > 150) {
|
|
496
|
-
const tail = msgText.slice(150).slice(-150);
|
|
497
|
-
if (msgText.length > 300) {
|
|
498
|
-
parts.push('...');
|
|
499
|
-
}
|
|
500
|
-
parts.push(tail.slice(0, 75));
|
|
501
|
-
if (tail.length > 75) {
|
|
502
|
-
parts.push(tail.slice(75));
|
|
221
|
+
// Get the device options for a new context.
|
|
222
|
+
const deviceOptions = getDeviceOptions(
|
|
223
|
+
deviceID || 'default', lowMotion ? 'reduce-motion' : 'no-preference'
|
|
224
|
+
);
|
|
225
|
+
// If the device is valid:
|
|
226
|
+
if (deviceOptions) {
|
|
227
|
+
// Open a context (i.e. browser tab), with reduced motion if specified.
|
|
228
|
+
const browserContext = await browser.newContext(deviceOptions);
|
|
229
|
+
// Prevent default timeouts.
|
|
230
|
+
browserContext.setDefaultTimeout(0);
|
|
231
|
+
// When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
|
|
232
|
+
browserContext.on('page', async page => {
|
|
233
|
+
// Ensure the report has a jobData property.
|
|
234
|
+
report.jobData ??= {};
|
|
235
|
+
report.jobData.logCount ??= 0;
|
|
236
|
+
report.jobData.logSize ??= 0;
|
|
237
|
+
report.jobData.errorLogCount ??= 0;
|
|
238
|
+
report.jobData.browserTabOptions ??= deviceOptions;
|
|
239
|
+
// Add any error events to the count of logging errors.
|
|
240
|
+
page.on('crash', () => {
|
|
241
|
+
report.jobData.errorLogCount++;
|
|
242
|
+
console.log('Page crashed');
|
|
243
|
+
});
|
|
244
|
+
page.on('pageerror', () => {
|
|
245
|
+
report.jobData.errorLogCount++;
|
|
246
|
+
});
|
|
247
|
+
page.on('requestfailed', () => {
|
|
248
|
+
report.jobData.errorLogCount++;
|
|
249
|
+
});
|
|
250
|
+
// If the page emits a message:
|
|
251
|
+
page.on('console', msg => {
|
|
252
|
+
const msgText = msg.text();
|
|
253
|
+
let indentedMsg = '';
|
|
254
|
+
// If debugging is on:
|
|
255
|
+
if (debug) {
|
|
256
|
+
// Log a summary of the message on the console.
|
|
257
|
+
const parts = [msgText.slice(0, 75)];
|
|
258
|
+
if (msgText.length > 75) {
|
|
259
|
+
parts.push(msgText.slice(75, 150));
|
|
260
|
+
if (msgText.length > 150) {
|
|
261
|
+
const tail = msgText.slice(150).slice(-150);
|
|
262
|
+
if (msgText.length > 300) {
|
|
263
|
+
parts.push('...');
|
|
264
|
+
}
|
|
265
|
+
parts.push(tail.slice(0, 75));
|
|
266
|
+
if (tail.length > 75) {
|
|
267
|
+
parts.push(tail.slice(75));
|
|
268
|
+
}
|
|
503
269
|
}
|
|
504
270
|
}
|
|
271
|
+
indentedMsg = parts.map(part => ` | ${part}`).join('\n');
|
|
272
|
+
console.log(`\n${indentedMsg}`);
|
|
505
273
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
274
|
+
// Add statistics on the message to the report.
|
|
275
|
+
const msgTextLC = msgText.toLowerCase();
|
|
276
|
+
const msgLength = msgText.length;
|
|
277
|
+
report.jobData.logCount++;
|
|
278
|
+
report.jobData.logSize += msgLength;
|
|
279
|
+
if (errorWords.some(word => msgTextLC.includes(word))) {
|
|
280
|
+
report.jobData.errorLogCount++;
|
|
281
|
+
report.jobData.errorLogSize += msgLength;
|
|
282
|
+
}
|
|
283
|
+
const msgLC = msgText.toLowerCase();
|
|
284
|
+
if (
|
|
285
|
+
msgText.includes('403') && (msgLC.includes('status')
|
|
286
|
+
|| msgLC.includes('prohibited'))
|
|
287
|
+
) {
|
|
288
|
+
report.jobData.prohibitedCount++;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
// Open the first page of the context.
|
|
293
|
+
const page = await browserContext.newPage();
|
|
294
|
+
try {
|
|
295
|
+
// Wait until it is stable.
|
|
296
|
+
await page.waitForLoadState('domcontentloaded', {timeout: 5000});
|
|
297
|
+
// Navigate to the specified URL.
|
|
298
|
+
const navResult = await goTo(report, page, url, 15000, 'domcontentloaded');
|
|
299
|
+
// If the navigation succeeded:
|
|
300
|
+
if (navResult.success) {
|
|
301
|
+
// Update the name of the current browser type and store it in the page.
|
|
302
|
+
page.browserTypeName = browserID;
|
|
303
|
+
// Return the response of the target server, the browser context, and the page.
|
|
304
|
+
return {
|
|
305
|
+
success: true,
|
|
306
|
+
response: navResult.response,
|
|
307
|
+
browserContext,
|
|
308
|
+
page
|
|
309
|
+
};
|
|
517
310
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
311
|
+
// Otherwise, if the navigation failed:
|
|
312
|
+
else {
|
|
313
|
+
// Return the error.
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
error: navResult.error
|
|
317
|
+
};
|
|
524
318
|
}
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
// Open the first page of the context.
|
|
528
|
-
const page = await browserContext.newPage();
|
|
529
|
-
try {
|
|
530
|
-
// Wait until it is stable.
|
|
531
|
-
await page.waitForLoadState('domcontentloaded', {timeout: 5000});
|
|
532
|
-
// Navigate to the specified URL.
|
|
533
|
-
const navResult = await goTo(report, page, url, 15000, 'domcontentloaded');
|
|
534
|
-
// If the navigation succeeded:
|
|
535
|
-
if (navResult.success) {
|
|
536
|
-
// Update the name of the current browser type and store it in the page.
|
|
537
|
-
page.browserTypeName = typeName;
|
|
538
|
-
// Return the response of the target server, the browser context, and the page.
|
|
539
|
-
return {
|
|
540
|
-
success: true,
|
|
541
|
-
response: navResult.response,
|
|
542
|
-
browserContext,
|
|
543
|
-
page
|
|
544
|
-
};
|
|
545
319
|
}
|
|
546
|
-
//
|
|
547
|
-
|
|
548
|
-
// Return
|
|
320
|
+
// If it fails to become stable after load:
|
|
321
|
+
catch(error) {
|
|
322
|
+
// Return this.
|
|
323
|
+
console.log(`ERROR: Blank page load in new tab timed out (${error.message})`);
|
|
549
324
|
return {
|
|
550
325
|
success: false,
|
|
551
|
-
error:
|
|
326
|
+
error: 'Blank page load in new tab timed out'
|
|
552
327
|
};
|
|
553
328
|
}
|
|
554
329
|
}
|
|
555
|
-
//
|
|
556
|
-
|
|
330
|
+
// Otherwise, i.e. if the device is invalid:
|
|
331
|
+
else {
|
|
557
332
|
// Return this.
|
|
558
|
-
console.log(`ERROR:
|
|
333
|
+
console.log(`ERROR: Device ${deviceID} invalid`);
|
|
559
334
|
return {
|
|
560
335
|
success: false,
|
|
561
|
-
error:
|
|
336
|
+
error: `${deviceID} device invalid`
|
|
562
337
|
};
|
|
563
338
|
}
|
|
564
339
|
}
|
|
565
340
|
// Otherwise, i.e. if it does not exist:
|
|
566
341
|
else {
|
|
567
342
|
// Return this.
|
|
568
|
-
console.log(`ERROR: Browser of type ${
|
|
343
|
+
console.log(`ERROR: Browser of type ${browserID} could not be launched`);
|
|
569
344
|
return {
|
|
570
345
|
success: false,
|
|
571
|
-
error: `${
|
|
346
|
+
error: `${browserID} browser launch failed`
|
|
572
347
|
};
|
|
573
348
|
}
|
|
574
349
|
};
|
|
@@ -745,7 +520,7 @@ const abortActs = async (report, actIndex) => {
|
|
|
745
520
|
report.jobData.abortTime = nowString();
|
|
746
521
|
report.jobData.abortedAct = actIndex;
|
|
747
522
|
report.jobData.aborted = true;
|
|
748
|
-
// Report the job
|
|
523
|
+
// Report that the job is aborted.
|
|
749
524
|
console.log('ERROR: Job aborted');
|
|
750
525
|
// Return an abortive act index.
|
|
751
526
|
return -2;
|
|
@@ -783,834 +558,806 @@ const addError = async(alsoLog, alsoAbort, report, actIndex, message) => {
|
|
|
783
558
|
};
|
|
784
559
|
// Recursively performs the acts in a report.
|
|
785
560
|
const doActs = async (report, actIndex, page) => {
|
|
786
|
-
// FUNCTION DEFINITION START
|
|
787
|
-
// Quits and reports the job being aborted.
|
|
788
|
-
const abortActs = async () => {
|
|
789
|
-
// Add data on the aborted act to the report.
|
|
790
|
-
report.jobData.abortTime = nowString();
|
|
791
|
-
report.jobData.abortedAct = actIndex;
|
|
792
|
-
report.jobData.aborted = true;
|
|
793
|
-
// Prevent performance of additional acts.
|
|
794
|
-
actIndex = -2;
|
|
795
|
-
// Report this.
|
|
796
|
-
console.log('ERROR: Job aborted');
|
|
797
|
-
};
|
|
798
|
-
// FUNCTION DEFINITION END
|
|
799
561
|
const {acts} = report;
|
|
800
562
|
// If any more acts are to be performed:
|
|
801
563
|
if (actIndex > -1 && actIndex < acts.length) {
|
|
802
564
|
// Identify the act to be performed.
|
|
803
565
|
const act = acts[actIndex];
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
566
|
+
const {type, which} = act;
|
|
567
|
+
const actSuffix = type === 'test' ? ` ${which}` : '';
|
|
568
|
+
const message = `>>>> ${type}${actSuffix}`;
|
|
569
|
+
// If granular reporting has been specified:
|
|
570
|
+
if (report.observe) {
|
|
571
|
+
// Notify the observer of the act and log it.
|
|
572
|
+
const whichParam = which ? `&which=${which}` : '';
|
|
573
|
+
const messageParams = `act=${type}${whichParam}`;
|
|
574
|
+
tellServer(report, messageParams, message);
|
|
575
|
+
}
|
|
576
|
+
// Otherwise, i.e. if granular reporting has not been specified:
|
|
577
|
+
else {
|
|
578
|
+
// Log the act.
|
|
579
|
+
console.log(message);
|
|
580
|
+
}
|
|
581
|
+
// Increment the count of acts performed.
|
|
582
|
+
actCount++;
|
|
583
|
+
act.startTime = Date.now();
|
|
584
|
+
// If the act is an index changer:
|
|
585
|
+
if (type === 'next') {
|
|
586
|
+
const condition = act.if;
|
|
587
|
+
const logSuffix = condition.length === 3 ? ` ${condition[1]} ${condition[2]}` : '';
|
|
588
|
+
console.log(`>> ${condition[0]}${logSuffix}`);
|
|
589
|
+
// Identify the act to be checked.
|
|
590
|
+
const ifActIndex = report.acts.map(act => act.type !== 'next').lastIndexOf(true);
|
|
591
|
+
// Determine whether its jump condition is true.
|
|
592
|
+
const truth = isTrue(report.acts[ifActIndex].result, condition);
|
|
593
|
+
// Add the result to the act.
|
|
594
|
+
act.result = {
|
|
595
|
+
property: condition[0],
|
|
596
|
+
relation: condition[1],
|
|
597
|
+
criterion: condition[2],
|
|
598
|
+
value: truth[0],
|
|
599
|
+
jumpRequired: truth[1]
|
|
600
|
+
};
|
|
601
|
+
// If the condition is true:
|
|
602
|
+
if (truth[1]) {
|
|
603
|
+
// If the performance of acts is to stop:
|
|
604
|
+
if (act.jump === 0) {
|
|
605
|
+
// Quit.
|
|
606
|
+
actIndex = -2;
|
|
810
607
|
}
|
|
811
|
-
|
|
812
|
-
|
|
608
|
+
// Otherwise, if there is a numerical jump:
|
|
609
|
+
else if (act.jump) {
|
|
610
|
+
// Set the act index accordingly.
|
|
611
|
+
actIndex += act.jump - 1;
|
|
612
|
+
}
|
|
613
|
+
// Otherwise, if there is a named next act:
|
|
614
|
+
else if (act.next) {
|
|
615
|
+
// Set the new index accordingly, or stop if it does not exist.
|
|
616
|
+
actIndex = acts.map(act => act.name).indexOf(act.next) - 1;
|
|
813
617
|
}
|
|
814
618
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
//
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
// Add the result to the act.
|
|
841
|
-
act.result = {
|
|
842
|
-
property: condition[0],
|
|
843
|
-
relation: condition[1],
|
|
844
|
-
criterion: condition[2],
|
|
845
|
-
value: truth[0],
|
|
846
|
-
jumpRequired: truth[1]
|
|
847
|
-
};
|
|
848
|
-
// If the condition is true:
|
|
849
|
-
if (truth[1]) {
|
|
850
|
-
// If the performance of acts is to stop:
|
|
851
|
-
if (act.jump === 0) {
|
|
852
|
-
// Quit.
|
|
853
|
-
actIndex = -2;
|
|
854
|
-
}
|
|
855
|
-
// Otherwise, if there is a numerical jump:
|
|
856
|
-
else if (act.jump) {
|
|
857
|
-
// Set the act index accordingly.
|
|
858
|
-
actIndex += act.jump - 1;
|
|
859
|
-
}
|
|
860
|
-
// Otherwise, if there is a named next act:
|
|
861
|
-
else if (act.next) {
|
|
862
|
-
// Set the new index accordingly, or stop if it does not exist.
|
|
863
|
-
actIndex = acts.map(act => act.name).indexOf(act.next) - 1;
|
|
864
|
-
}
|
|
619
|
+
}
|
|
620
|
+
// Otherwise, if the act is a launch:
|
|
621
|
+
else if (type === 'launch') {
|
|
622
|
+
// Launch the specified browser on the specified device and navigate to the specified URL.
|
|
623
|
+
const launchResult = await launch(
|
|
624
|
+
report,
|
|
625
|
+
act.url || report.sources.target.url,
|
|
626
|
+
debug,
|
|
627
|
+
waits,
|
|
628
|
+
act.deviceID || report.deviceID,
|
|
629
|
+
act.browserID || report.browserID,
|
|
630
|
+
act.lowMotion || report.lowMotion
|
|
631
|
+
);
|
|
632
|
+
// If the launch and navigation succeeded:
|
|
633
|
+
if (launchResult && launchResult.success) {
|
|
634
|
+
// Get the response of the target server.
|
|
635
|
+
const {response} = launchResult;
|
|
636
|
+
// Get the target page.
|
|
637
|
+
page = launchResult.page;
|
|
638
|
+
// Add the actual URL to the act.
|
|
639
|
+
act.actualURL = page.url();
|
|
640
|
+
// Add the script nonce, if any, to the act.
|
|
641
|
+
const scriptNonce = await getNonce(response);
|
|
642
|
+
if (scriptNonce) {
|
|
643
|
+
report.jobData.lastScriptNonce = scriptNonce;
|
|
865
644
|
}
|
|
866
645
|
}
|
|
867
|
-
// Otherwise, if the
|
|
868
|
-
else
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
report,
|
|
872
|
-
act.which,
|
|
873
|
-
act.url,
|
|
874
|
-
debug,
|
|
875
|
-
waits,
|
|
876
|
-
act.deviceID || 'default',
|
|
877
|
-
act.lowMotion ? 'reduce' : 'no-preference'
|
|
646
|
+
// Otherwise, i.e. if the launch or navigation failed:
|
|
647
|
+
else {
|
|
648
|
+
// Add an error result to the act and abort the job.
|
|
649
|
+
actIndex = await addError(
|
|
650
|
+
true, true, report, actIndex, `ERROR: Launch failed (${launchResult.error})`
|
|
878
651
|
);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Otherwise, if a current page exists:
|
|
655
|
+
else if (page) {
|
|
656
|
+
// If the act is navigation to a url:
|
|
657
|
+
if (act.type === 'url') {
|
|
658
|
+
// Identify the URL.
|
|
659
|
+
const resolved = act.which.replace('__dirname', __dirname);
|
|
660
|
+
requestedURL = resolved;
|
|
661
|
+
// Visit it and wait until the DOM is loaded.
|
|
662
|
+
const navResult = await goTo(report, page, requestedURL, 15000, 'domcontentloaded');
|
|
663
|
+
// If the visit succeeded:
|
|
664
|
+
if (navResult.success) {
|
|
887
665
|
// Add the script nonce, if any, to the act.
|
|
888
|
-
const
|
|
666
|
+
const {response} = navResult;
|
|
667
|
+
const scriptNonce = getNonce(response);
|
|
889
668
|
if (scriptNonce) {
|
|
890
669
|
report.jobData.lastScriptNonce = scriptNonce;
|
|
891
670
|
}
|
|
671
|
+
// Add the resulting URL to the act.
|
|
672
|
+
if (! act.result) {
|
|
673
|
+
act.result = {};
|
|
674
|
+
}
|
|
675
|
+
act.result.url = page.url();
|
|
676
|
+
// If a prohibited redirection occurred:
|
|
677
|
+
if (response.exception === 'badRedirection') {
|
|
678
|
+
// Report this and abort the job.
|
|
679
|
+
actIndex = await addError(
|
|
680
|
+
true, true, report, actIndex, 'ERROR: Navigation illicitly redirected'
|
|
681
|
+
);
|
|
682
|
+
}
|
|
892
683
|
}
|
|
893
|
-
// Otherwise, i.e. if the
|
|
684
|
+
// Otherwise, i.e. if the visit failed:
|
|
894
685
|
else {
|
|
895
|
-
//
|
|
896
|
-
actIndex = await addError(
|
|
897
|
-
true, true, report, actIndex, `ERROR: Launch failed (${launchResult.error})`
|
|
898
|
-
);
|
|
686
|
+
// Report this and abort the job.
|
|
687
|
+
actIndex = await addError(true, true, report, actIndex, 'ERROR: Visit failed');
|
|
899
688
|
}
|
|
900
689
|
}
|
|
901
|
-
// Otherwise, if a
|
|
902
|
-
else if (
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
const {response} = navResult;
|
|
914
|
-
const scriptNonce = getNonce(response);
|
|
915
|
-
if (scriptNonce) {
|
|
916
|
-
report.jobData.lastScriptNonce = scriptNonce;
|
|
917
|
-
}
|
|
918
|
-
// Add the resulting URL to the act.
|
|
919
|
-
if (! act.result) {
|
|
920
|
-
act.result = {};
|
|
921
|
-
}
|
|
922
|
-
act.result.url = page.url();
|
|
923
|
-
// If a prohibited redirection occurred:
|
|
924
|
-
if (response.exception === 'badRedirection') {
|
|
925
|
-
// Report this and abort the job.
|
|
926
|
-
actIndex = await addError(
|
|
927
|
-
true, true, report, actIndex, 'ERROR: Navigation illicitly redirected'
|
|
928
|
-
);
|
|
929
|
-
}
|
|
690
|
+
// Otherwise, if the act is a wait for text:
|
|
691
|
+
else if (act.type === 'wait') {
|
|
692
|
+
const {what, which} = act;
|
|
693
|
+
console.log(`>> ${what}`);
|
|
694
|
+
const result = act.result = {};
|
|
695
|
+
// If the text is to be the URL:
|
|
696
|
+
if (what === 'url') {
|
|
697
|
+
// Wait for the URL to be the exact text.
|
|
698
|
+
try {
|
|
699
|
+
await page.waitForURL(which, {timeout: 15000});
|
|
700
|
+
result.found = true;
|
|
701
|
+
result.url = page.url();
|
|
930
702
|
}
|
|
931
|
-
//
|
|
932
|
-
|
|
933
|
-
//
|
|
934
|
-
actIndex = await
|
|
703
|
+
// If the wait times out:
|
|
704
|
+
catch(error) {
|
|
705
|
+
// Quit.
|
|
706
|
+
actIndex = await abortActs(report, actIndex);
|
|
707
|
+
waitError(page, act, error, 'text in the URL');
|
|
935
708
|
}
|
|
936
709
|
}
|
|
937
|
-
// Otherwise, if the
|
|
938
|
-
else if (
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
await abortActs();
|
|
954
|
-
waitError(page, act, error, 'text in the URL');
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
// Otherwise, if the text is to be a substring of the page title:
|
|
958
|
-
else if (what === 'title') {
|
|
959
|
-
// Wait for the page title to include the text, case-insensitively.
|
|
960
|
-
try {
|
|
961
|
-
await page.waitForFunction(
|
|
962
|
-
text => document
|
|
963
|
-
&& document.title
|
|
964
|
-
&& document.title.toLowerCase().includes(text.toLowerCase()),
|
|
965
|
-
which,
|
|
966
|
-
{
|
|
967
|
-
polling: 1000,
|
|
968
|
-
timeout: 5000
|
|
969
|
-
}
|
|
970
|
-
);
|
|
971
|
-
result.found = true;
|
|
972
|
-
result.title = await page.title();
|
|
973
|
-
}
|
|
974
|
-
// If the wait times out:
|
|
975
|
-
catch(error) {
|
|
976
|
-
// Quit.
|
|
977
|
-
await abortActs();
|
|
978
|
-
waitError(page, act, error, 'text in the title');
|
|
979
|
-
}
|
|
710
|
+
// Otherwise, if the text is to be a substring of the page title:
|
|
711
|
+
else if (what === 'title') {
|
|
712
|
+
// Wait for the page title to include the text, case-insensitively.
|
|
713
|
+
try {
|
|
714
|
+
await page.waitForFunction(
|
|
715
|
+
text => document
|
|
716
|
+
&& document.title
|
|
717
|
+
&& document.title.toLowerCase().includes(text.toLowerCase()),
|
|
718
|
+
which,
|
|
719
|
+
{
|
|
720
|
+
polling: 1000,
|
|
721
|
+
timeout: 5000
|
|
722
|
+
}
|
|
723
|
+
);
|
|
724
|
+
result.found = true;
|
|
725
|
+
result.title = await page.title();
|
|
980
726
|
}
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
//
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
text => document
|
|
987
|
-
&& document.body
|
|
988
|
-
&& document.body.innerText.toLowerCase().includes(text.toLowerCase()),
|
|
989
|
-
which,
|
|
990
|
-
{
|
|
991
|
-
polling: 2000,
|
|
992
|
-
timeout: 15000
|
|
993
|
-
}
|
|
994
|
-
);
|
|
995
|
-
result.found = true;
|
|
996
|
-
}
|
|
997
|
-
// If the wait times out:
|
|
998
|
-
catch(error) {
|
|
999
|
-
// Quit.
|
|
1000
|
-
await abortActs();
|
|
1001
|
-
waitError(page, act, error, 'text in the body');
|
|
1002
|
-
}
|
|
727
|
+
// If the wait times out:
|
|
728
|
+
catch(error) {
|
|
729
|
+
// Quit.
|
|
730
|
+
actIndex = await abortActs(report, actIndex);
|
|
731
|
+
waitError(page, act, error, 'text in the title');
|
|
1003
732
|
}
|
|
1004
733
|
}
|
|
1005
|
-
// Otherwise, if the
|
|
1006
|
-
else if (
|
|
1007
|
-
// Wait for
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
734
|
+
// Otherwise, if the text is to be a substring of the text of the page body:
|
|
735
|
+
else if (what === 'body') {
|
|
736
|
+
// Wait for the body to include the text, case-insensitively.
|
|
737
|
+
try {
|
|
738
|
+
await page.waitForFunction(
|
|
739
|
+
text => document
|
|
740
|
+
&& document.body
|
|
741
|
+
&& document.body.innerText.toLowerCase().includes(text.toLowerCase()),
|
|
742
|
+
which,
|
|
743
|
+
{
|
|
744
|
+
polling: 2000,
|
|
745
|
+
timeout: 15000
|
|
746
|
+
}
|
|
1018
747
|
);
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
};
|
|
748
|
+
result.found = true;
|
|
749
|
+
}
|
|
750
|
+
// If the wait times out:
|
|
751
|
+
catch(error) {
|
|
752
|
+
// Quit.
|
|
753
|
+
actIndex = await abortActs(report, actIndex);
|
|
754
|
+
waitError(page, act, error, 'text in the body');
|
|
1027
755
|
}
|
|
1028
756
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
757
|
+
}
|
|
758
|
+
// Otherwise, if the act is a wait for a state:
|
|
759
|
+
else if (act.type === 'state') {
|
|
760
|
+
// Wait for it.
|
|
761
|
+
const stateIndex = ['loaded', 'idle'].indexOf(act.which);
|
|
762
|
+
await page.waitForLoadState(
|
|
763
|
+
['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 15000][stateIndex]}
|
|
764
|
+
)
|
|
765
|
+
// If the wait times out:
|
|
766
|
+
.catch(async error => {
|
|
767
|
+
// Report this and abort the job.
|
|
768
|
+
console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
|
|
769
|
+
actIndex = await addError(
|
|
770
|
+
true, true, report, actIndex, `ERROR waiting for page to be ${act.which}`
|
|
771
|
+
);
|
|
772
|
+
});
|
|
773
|
+
// If the wait succeeded:
|
|
774
|
+
if (actIndex > -2) {
|
|
775
|
+
// Add state data to the report.
|
|
776
|
+
act.result = {
|
|
777
|
+
success: true,
|
|
778
|
+
state: act.which
|
|
1038
779
|
};
|
|
1039
|
-
act.result = result;
|
|
1040
780
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
.
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
};
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
// Otherwise, if the act performs tests of a tool:
|
|
1071
|
-
else if (act.type === 'test') {
|
|
1072
|
-
// Add a description of the tool to the act.
|
|
1073
|
-
act.what = tools[act.which];
|
|
1074
|
-
// Initialize the options argument.
|
|
1075
|
-
const options = {
|
|
1076
|
-
report,
|
|
1077
|
-
act
|
|
1078
|
-
};
|
|
1079
|
-
// Add any specified arguments to it.
|
|
1080
|
-
Object.keys(act).forEach(key => {
|
|
1081
|
-
if (! ['type', 'which'].includes(key)) {
|
|
1082
|
-
options[key] = act[key];
|
|
781
|
+
}
|
|
782
|
+
// Otherwise, if the act is a page switch:
|
|
783
|
+
else if (act.type === 'page') {
|
|
784
|
+
// Wait for a page to be created and identify it as current.
|
|
785
|
+
page = await browserContext.waitForEvent('page');
|
|
786
|
+
// Wait until it is idle.
|
|
787
|
+
await page.waitForLoadState('networkidle', {timeout: 15000});
|
|
788
|
+
// Add the resulting URL to the act.
|
|
789
|
+
const result = {
|
|
790
|
+
url: page.url()
|
|
791
|
+
};
|
|
792
|
+
act.result = result;
|
|
793
|
+
}
|
|
794
|
+
// Otherwise, if the page has a URL:
|
|
795
|
+
else if (page.url() && page.url() !== 'about:blank') {
|
|
796
|
+
const url = page.url();
|
|
797
|
+
// Add the URL to the act.
|
|
798
|
+
act.actualURL = url;
|
|
799
|
+
// If the act is a revelation:
|
|
800
|
+
if (act.type === 'reveal') {
|
|
801
|
+
// Make all elements in the page visible.
|
|
802
|
+
await page.$$eval('body *', elements => {
|
|
803
|
+
elements.forEach(element => {
|
|
804
|
+
const styleDec = window.getComputedStyle(element);
|
|
805
|
+
if (styleDec.display === 'none') {
|
|
806
|
+
element.style.display = 'initial';
|
|
1083
807
|
}
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
const startTime = Date.now();
|
|
1087
|
-
// Perform the specified tests of the tool and get a report.
|
|
1088
|
-
try {
|
|
1089
|
-
const actReport = await require(`./tests/${act.which}`).reporter(page, options);
|
|
1090
|
-
// Import its test results and process data into the act.
|
|
1091
|
-
act.result = actReport && actReport.result || {};
|
|
1092
|
-
act.data = actReport && actReport.data || {};
|
|
1093
|
-
// If the page prevented the tool from operating:
|
|
1094
|
-
if (act.data.prevented) {
|
|
1095
|
-
// Add prevention data to the job data.
|
|
1096
|
-
report.jobData.preventions[act.which] = act.data.error;
|
|
808
|
+
if (['hidden', 'collapse'].includes(styleDec.visibility)) {
|
|
809
|
+
element.style.visibility = 'inherit';
|
|
1097
810
|
}
|
|
811
|
+
});
|
|
812
|
+
act.result = {
|
|
813
|
+
success: true
|
|
814
|
+
};
|
|
815
|
+
})
|
|
816
|
+
.catch(error => {
|
|
817
|
+
console.log(`ERROR making all elements visible (${error.message})`);
|
|
818
|
+
act.result = {
|
|
819
|
+
success: false
|
|
820
|
+
};
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
// Otherwise, if the act performs tests of a tool:
|
|
824
|
+
else if (act.type === 'test') {
|
|
825
|
+
// Add a description of the tool to the act.
|
|
826
|
+
act.what = tools[act.which];
|
|
827
|
+
// Initialize the options argument.
|
|
828
|
+
const options = {
|
|
829
|
+
report,
|
|
830
|
+
act
|
|
831
|
+
};
|
|
832
|
+
// Add any specified arguments to it.
|
|
833
|
+
Object.keys(act).forEach(key => {
|
|
834
|
+
if (! ['type', 'which'].includes(key)) {
|
|
835
|
+
options[key] = act[key];
|
|
1098
836
|
}
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
837
|
+
});
|
|
838
|
+
// Get the start time of the act.
|
|
839
|
+
const startTime = Date.now();
|
|
840
|
+
// Perform the specified tests of the tool and get a report.
|
|
841
|
+
try {
|
|
842
|
+
const actReport = await require(`./tests/${act.which}`).reporter(page, options);
|
|
843
|
+
// Import its test results and process data into the act.
|
|
844
|
+
act.result = actReport && actReport.result || {};
|
|
845
|
+
act.data = actReport && actReport.data || {};
|
|
846
|
+
// If the page prevented the tool from operating:
|
|
847
|
+
if (act.data.prevented) {
|
|
848
|
+
// Add prevention data to the job data.
|
|
849
|
+
report.jobData.preventions[act.which] = act.data.error;
|
|
1111
850
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
851
|
+
}
|
|
852
|
+
// If the testing failed:
|
|
853
|
+
catch(error) {
|
|
854
|
+
// Report this.
|
|
855
|
+
const message = error.message.slice(0, 400);
|
|
856
|
+
console.log(`ERROR: Test act ${act.which} failed (${message})`);
|
|
857
|
+
act.data.error = act.data.error ? `${act.data.error}; ${message}` : message;
|
|
858
|
+
}
|
|
859
|
+
// Add the elapsed time of the tool to the report.
|
|
860
|
+
const time = Math.round((Date.now() - startTime) / 1000);
|
|
861
|
+
const {toolTimes} = report.jobData;
|
|
862
|
+
if (! toolTimes[act.which]) {
|
|
863
|
+
toolTimes[act.which] = 0;
|
|
864
|
+
}
|
|
865
|
+
toolTimes[act.which] += time;
|
|
866
|
+
// If a standard-format result is to be included in the report:
|
|
867
|
+
const standard = report.standard || 'only';
|
|
868
|
+
if (['also', 'only'].includes(standard)) {
|
|
869
|
+
// Initialize it.
|
|
870
|
+
act.standardResult = {
|
|
871
|
+
totals: [0, 0, 0, 0],
|
|
872
|
+
instances: []
|
|
873
|
+
};
|
|
874
|
+
// Populate it.
|
|
875
|
+
standardize(act);
|
|
876
|
+
// Add a box ID and a path ID to each of its standard instances if missing.
|
|
877
|
+
for (const instance of act.standardResult.instances) {
|
|
878
|
+
const elementID = await identify(instance, page);
|
|
879
|
+
if (! instance.boxID) {
|
|
880
|
+
instance.boxID = elementID ? elementID.boxID : '';
|
|
881
|
+
}
|
|
882
|
+
if (! instance.pathID) {
|
|
883
|
+
instance.pathID = elementID ? elementID.pathID : '';
|
|
1137
884
|
}
|
|
885
|
+
};
|
|
886
|
+
// If the original-format result is not to be included in the report:
|
|
887
|
+
if (standard === 'only') {
|
|
888
|
+
// Remove it.
|
|
889
|
+
delete act.result;
|
|
1138
890
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
if (! truth[1]) {
|
|
1156
|
-
failureCount++;
|
|
1157
|
-
}
|
|
891
|
+
}
|
|
892
|
+
// If the act has expectations:
|
|
893
|
+
const expectations = act.expect;
|
|
894
|
+
if (expectations) {
|
|
895
|
+
// Initialize whether they were fulfilled.
|
|
896
|
+
act.expectations = [];
|
|
897
|
+
let failureCount = 0;
|
|
898
|
+
// For each expectation:
|
|
899
|
+
expectations.forEach(spec => {
|
|
900
|
+
const truth = isTrue(act, spec);
|
|
901
|
+
act.expectations.push({
|
|
902
|
+
property: spec[0],
|
|
903
|
+
relation: spec[1],
|
|
904
|
+
criterion: spec[2],
|
|
905
|
+
actual: truth[0],
|
|
906
|
+
passed: truth[1]
|
|
1158
907
|
});
|
|
1159
|
-
|
|
1160
|
-
|
|
908
|
+
if (! truth[1]) {
|
|
909
|
+
failureCount++;
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
act.expectationFailures = failureCount;
|
|
1161
913
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
// If no element satisfied the specifications:
|
|
1198
|
-
if (! act.result.found) {
|
|
1199
|
-
// Add the failure data to the report.
|
|
1200
|
-
act.result.success = false;
|
|
1201
|
-
act.result.error = 'exhausted';
|
|
1202
|
-
act.result.typeElementCount = selections.length;
|
|
1203
|
-
if (slimText) {
|
|
1204
|
-
act.result.textElementCount = --matchCount;
|
|
914
|
+
}
|
|
915
|
+
// Otherwise, if the act is a move:
|
|
916
|
+
else if (moves[act.type]) {
|
|
917
|
+
const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
|
|
918
|
+
// Try up to 5 times to:
|
|
919
|
+
act.result = {found: false};
|
|
920
|
+
let selection = {};
|
|
921
|
+
let tries = 0;
|
|
922
|
+
const slimText = act.which ? debloat(act.which) : '';
|
|
923
|
+
while (tries++ < 5 && ! act.result.found) {
|
|
924
|
+
if (page) {
|
|
925
|
+
// Identify the elements of the specified type.
|
|
926
|
+
const selections = await page.$$(selector);
|
|
927
|
+
// If there are any:
|
|
928
|
+
if (selections.length) {
|
|
929
|
+
// If there are enough to make a match possible:
|
|
930
|
+
if ((act.index || 0) < selections.length) {
|
|
931
|
+
// For each element of the specified type:
|
|
932
|
+
let matchCount = 0;
|
|
933
|
+
const selectionTexts = [];
|
|
934
|
+
for (selection of selections) {
|
|
935
|
+
// Add its lower-case text or an empty string to the list of element texts.
|
|
936
|
+
const selectionText = slimText ? await textOf(page, selection) : '';
|
|
937
|
+
selectionTexts.push(selectionText);
|
|
938
|
+
// If its text includes any specified text, case-insensitively:
|
|
939
|
+
if (selectionText.includes(slimText)) {
|
|
940
|
+
// If the element has the specified index among such elements:
|
|
941
|
+
if (matchCount++ === (act.index || 0)) {
|
|
942
|
+
// Report it as the matching element and stop checking.
|
|
943
|
+
act.result.found = true;
|
|
944
|
+
act.result.textSpec = slimText;
|
|
945
|
+
act.result.textContent = selectionText;
|
|
946
|
+
break;
|
|
1205
947
|
}
|
|
1206
|
-
act.result.message = 'Not enough specified elements exist';
|
|
1207
|
-
act.result.candidateTexts = selectionTexts;
|
|
1208
948
|
}
|
|
1209
949
|
}
|
|
1210
|
-
//
|
|
1211
|
-
|
|
950
|
+
// If no element satisfied the specifications:
|
|
951
|
+
if (! act.result.found) {
|
|
1212
952
|
// Add the failure data to the report.
|
|
1213
953
|
act.result.success = false;
|
|
1214
|
-
act.result.error = '
|
|
954
|
+
act.result.error = 'exhausted';
|
|
1215
955
|
act.result.typeElementCount = selections.length;
|
|
1216
|
-
|
|
956
|
+
if (slimText) {
|
|
957
|
+
act.result.textElementCount = --matchCount;
|
|
958
|
+
}
|
|
959
|
+
act.result.message = 'Not enough specified elements exist';
|
|
960
|
+
act.result.candidateTexts = selectionTexts;
|
|
1217
961
|
}
|
|
1218
962
|
}
|
|
1219
|
-
// Otherwise, i.e. if there are
|
|
963
|
+
// Otherwise, i.e. if there are too few such elements to make a match possible:
|
|
1220
964
|
else {
|
|
1221
965
|
// Add the failure data to the report.
|
|
1222
966
|
act.result.success = false;
|
|
1223
|
-
act.result.error = '
|
|
1224
|
-
act.result.typeElementCount =
|
|
1225
|
-
act.result.message = '
|
|
967
|
+
act.result.error = 'fewer';
|
|
968
|
+
act.result.typeElementCount = selections.length;
|
|
969
|
+
act.result.message = 'Elements of specified type too few';
|
|
1226
970
|
}
|
|
1227
971
|
}
|
|
1228
|
-
// Otherwise, i.e. if
|
|
972
|
+
// Otherwise, i.e. if there are no elements of the specified type:
|
|
1229
973
|
else {
|
|
1230
974
|
// Add the failure data to the report.
|
|
1231
975
|
act.result.success = false;
|
|
1232
|
-
act.result.error = '
|
|
1233
|
-
act.result.
|
|
1234
|
-
|
|
1235
|
-
if (! act.result.found) {
|
|
1236
|
-
await wait(2000);
|
|
976
|
+
act.result.error = 'none';
|
|
977
|
+
act.result.typeElementCount = 0;
|
|
978
|
+
act.result.message = 'No elements of specified type found';
|
|
1237
979
|
}
|
|
1238
980
|
}
|
|
1239
|
-
//
|
|
1240
|
-
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
981
|
+
// Otherwise, i.e. if the page no longer exists:
|
|
982
|
+
else {
|
|
983
|
+
// Add the failure data to the report.
|
|
984
|
+
act.result.success = false;
|
|
985
|
+
act.result.error = 'gone';
|
|
986
|
+
act.result.message = 'Page gone';
|
|
987
|
+
}
|
|
988
|
+
if (! act.result.found) {
|
|
989
|
+
await wait(2000);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
// If a match was found:
|
|
993
|
+
if (act.result.found) {
|
|
994
|
+
// FUNCTION DEFINITION START
|
|
995
|
+
// Performs a click or Enter keypress and waits for the network to be idle.
|
|
996
|
+
const doAndWait = async isClick => {
|
|
997
|
+
// Perform and report the move.
|
|
998
|
+
const move = isClick ? 'click' : 'Enter keypress';
|
|
999
|
+
try {
|
|
1000
|
+
await isClick
|
|
1001
|
+
? selection.click({timeout: 4000})
|
|
1002
|
+
: selection.press('Enter', {timeout: 4000});
|
|
1003
|
+
act.result.success = true;
|
|
1004
|
+
act.result.move = move;
|
|
1005
|
+
}
|
|
1006
|
+
// If the move fails:
|
|
1007
|
+
catch(error) {
|
|
1008
|
+
// Add the error result to the act and abort the job.
|
|
1009
|
+
actIndex = await addError(true, true, report, actIndex, `ERROR: ${move} failed`);
|
|
1010
|
+
}
|
|
1011
|
+
if (act.result.success) {
|
|
1246
1012
|
try {
|
|
1247
|
-
await
|
|
1248
|
-
|
|
1249
|
-
: selection.press('Enter', {timeout: 4000});
|
|
1250
|
-
act.result.success = true;
|
|
1251
|
-
act.result.move = move;
|
|
1013
|
+
await page.context().waitForEvent('networkidle', {timeout: 10000});
|
|
1014
|
+
act.result.idleTimely = true;
|
|
1252
1015
|
}
|
|
1253
|
-
// If the move fails:
|
|
1254
1016
|
catch(error) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
}
|
|
1258
|
-
if (act.result.success) {
|
|
1259
|
-
try {
|
|
1260
|
-
await page.context().waitForEvent('networkidle', {timeout: 10000});
|
|
1261
|
-
act.result.idleTimely = true;
|
|
1262
|
-
}
|
|
1263
|
-
catch(error) {
|
|
1264
|
-
console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
|
|
1265
|
-
act.result.idleTimely = false;
|
|
1266
|
-
}
|
|
1267
|
-
// If the move created a new page, make it current.
|
|
1268
|
-
page = currentPage;
|
|
1269
|
-
act.result.newURL = page.url();
|
|
1017
|
+
console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
|
|
1018
|
+
act.result.idleTimely = false;
|
|
1270
1019
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
if (act.type === 'button') {
|
|
1275
|
-
await selection.click({timeout: 3000});
|
|
1276
|
-
act.result.success = true;
|
|
1277
|
-
act.result.move = 'clicked';
|
|
1020
|
+
// If the move created a new page, make it current.
|
|
1021
|
+
page = currentPage;
|
|
1022
|
+
act.result.newURL = page.url();
|
|
1278
1023
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
else {
|
|
1305
|
-
const report = `ERROR: could not check ${act.type} because disabled`;
|
|
1306
|
-
console.log(report);
|
|
1024
|
+
};
|
|
1025
|
+
// FUNCTION DEFINITION END
|
|
1026
|
+
// If the move is a button click, perform it.
|
|
1027
|
+
if (act.type === 'button') {
|
|
1028
|
+
await selection.click({timeout: 3000});
|
|
1029
|
+
act.result.success = true;
|
|
1030
|
+
act.result.move = 'clicked';
|
|
1031
|
+
}
|
|
1032
|
+
// Otherwise, if it is checking a radio button or checkbox, perform it.
|
|
1033
|
+
else if (['checkbox', 'radio'].includes(act.type)) {
|
|
1034
|
+
await selection.waitForElementState('stable', {timeout: 2000})
|
|
1035
|
+
.catch(error => {
|
|
1036
|
+
console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
|
|
1037
|
+
act.result.success = false;
|
|
1038
|
+
act.result.error = `ERROR waiting for stable ${act.type}`;
|
|
1039
|
+
});
|
|
1040
|
+
if (! act.result.error) {
|
|
1041
|
+
const isEnabled = await selection.isEnabled();
|
|
1042
|
+
if (isEnabled) {
|
|
1043
|
+
await selection.check({
|
|
1044
|
+
force: true,
|
|
1045
|
+
timeout: 2000
|
|
1046
|
+
})
|
|
1047
|
+
.catch(error => {
|
|
1048
|
+
console.log(`ERROR checking ${act.type} (${error.message})`);
|
|
1307
1049
|
act.result.success = false;
|
|
1308
|
-
act.result.error =
|
|
1050
|
+
act.result.error = `ERROR checking ${act.type}`;
|
|
1051
|
+
});
|
|
1052
|
+
if (! act.result.error) {
|
|
1053
|
+
act.result.success = true;
|
|
1054
|
+
act.result.move = 'checked';
|
|
1309
1055
|
}
|
|
1310
1056
|
}
|
|
1057
|
+
else {
|
|
1058
|
+
const report = `ERROR: could not check ${act.type} because disabled`;
|
|
1059
|
+
console.log(report);
|
|
1060
|
+
act.result.success = false;
|
|
1061
|
+
act.result.error = report;
|
|
1062
|
+
}
|
|
1311
1063
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1064
|
+
}
|
|
1065
|
+
// Otherwise, if it is focusing the element, perform it.
|
|
1066
|
+
else if (act.type === 'focus') {
|
|
1067
|
+
await selection.focus({timeout: 2000});
|
|
1068
|
+
act.result.success = true;
|
|
1069
|
+
act.result.move = 'focused';
|
|
1070
|
+
}
|
|
1071
|
+
// Otherwise, if it is clicking a link:
|
|
1072
|
+
else if (act.type === 'link') {
|
|
1073
|
+
const href = await selection.getAttribute('href');
|
|
1074
|
+
const target = await selection.getAttribute('target');
|
|
1075
|
+
act.result.href = href || 'NONE';
|
|
1076
|
+
act.result.target = target || 'DEFAULT';
|
|
1077
|
+
// If the destination is a new page:
|
|
1078
|
+
if (target && target !== '_self') {
|
|
1079
|
+
// Click the link and wait for the network to be idle.
|
|
1080
|
+
doAndWait(true);
|
|
1317
1081
|
}
|
|
1318
|
-
// Otherwise, if
|
|
1319
|
-
else
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1082
|
+
// Otherwise, i.e. if the destination is in the current page:
|
|
1083
|
+
else {
|
|
1084
|
+
// Click the link and wait for the resulting navigation.
|
|
1085
|
+
try {
|
|
1086
|
+
await selection.click({timeout: 5000});
|
|
1087
|
+
// Wait for the new content to load.
|
|
1088
|
+
await page.waitForLoadState('domcontentloaded', {timeout: 6000});
|
|
1089
|
+
act.result.success = true;
|
|
1090
|
+
act.result.move = 'clicked';
|
|
1091
|
+
act.result.newURL = page.url();
|
|
1328
1092
|
}
|
|
1329
|
-
//
|
|
1330
|
-
|
|
1331
|
-
//
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
act.result.move = 'clicked';
|
|
1338
|
-
act.result.newURL = page.url();
|
|
1339
|
-
}
|
|
1340
|
-
// If the click or load failed:
|
|
1341
|
-
catch(error) {
|
|
1342
|
-
// Quit and add failure data to the report.
|
|
1343
|
-
console.log(`ERROR clicking link (${errorStart(error)})`);
|
|
1344
|
-
act.result.success = false;
|
|
1345
|
-
act.result.error = 'unclickable';
|
|
1346
|
-
act.result.message = 'ERROR: click or load timed out';
|
|
1347
|
-
await abortActs();
|
|
1348
|
-
}
|
|
1349
|
-
// If the link click succeeded:
|
|
1350
|
-
if (! act.result.error) {
|
|
1351
|
-
// Add success data to the report.
|
|
1352
|
-
act.result.success = true;
|
|
1353
|
-
act.result.move = 'clicked';
|
|
1354
|
-
}
|
|
1093
|
+
// If the click or load failed:
|
|
1094
|
+
catch(error) {
|
|
1095
|
+
// Quit and add failure data to the report.
|
|
1096
|
+
console.log(`ERROR clicking link (${errorStart(error)})`);
|
|
1097
|
+
act.result.success = false;
|
|
1098
|
+
act.result.error = 'unclickable';
|
|
1099
|
+
act.result.message = 'ERROR: click or load timed out';
|
|
1100
|
+
actIndex = await abortActs(report, actIndex);
|
|
1355
1101
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
if (options && Array.isArray(options) && options.length) {
|
|
1362
|
-
const optionTexts = [];
|
|
1363
|
-
for (const option of options) {
|
|
1364
|
-
const optionText = await option.textContent();
|
|
1365
|
-
optionTexts.push(optionText);
|
|
1366
|
-
}
|
|
1367
|
-
const matchTexts = optionTexts.map(
|
|
1368
|
-
(text, index) => text.includes(act.what) ? index : -1
|
|
1369
|
-
);
|
|
1370
|
-
const index = matchTexts.filter(text => text > -1)[act.index || 0];
|
|
1371
|
-
if (index !== undefined) {
|
|
1372
|
-
await selection.selectOption({index});
|
|
1373
|
-
optionText = optionTexts[index];
|
|
1374
|
-
}
|
|
1102
|
+
// If the link click succeeded:
|
|
1103
|
+
if (! act.result.error) {
|
|
1104
|
+
// Add success data to the report.
|
|
1105
|
+
act.result.success = true;
|
|
1106
|
+
act.result.move = 'clicked';
|
|
1375
1107
|
}
|
|
1376
|
-
act.result.success = true;
|
|
1377
|
-
act.result.move = 'selected';
|
|
1378
|
-
act.result.option = optionText;
|
|
1379
1108
|
}
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
const
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
// If the text contains a placeholder for an environment variable:
|
|
1391
|
-
let {what} = act;
|
|
1392
|
-
if (/__[A-Z]+__/.test(what)) {
|
|
1393
|
-
// Replace it.
|
|
1394
|
-
const envKey = /__([A-Z]+)__/.exec(what)[1];
|
|
1395
|
-
const envValue = process.env[envKey];
|
|
1396
|
-
what = what.replace(/__[A-Z]+__/, envValue);
|
|
1109
|
+
}
|
|
1110
|
+
// Otherwise, if it is selecting an option in a select list, perform it.
|
|
1111
|
+
else if (act.type === 'select') {
|
|
1112
|
+
const options = await selection.$$('option');
|
|
1113
|
+
let optionText = '';
|
|
1114
|
+
if (options && Array.isArray(options) && options.length) {
|
|
1115
|
+
const optionTexts = [];
|
|
1116
|
+
for (const option of options) {
|
|
1117
|
+
const optionText = await option.textContent();
|
|
1118
|
+
optionTexts.push(optionText);
|
|
1397
1119
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
act.
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
// Press the Enter key and wait for a network to be idle.
|
|
1406
|
-
doAndWait(false);
|
|
1120
|
+
const matchTexts = optionTexts.map(
|
|
1121
|
+
(text, index) => text.includes(act.what) ? index : -1
|
|
1122
|
+
);
|
|
1123
|
+
const index = matchTexts.filter(text => text > -1)[act.index || 0];
|
|
1124
|
+
if (index !== undefined) {
|
|
1125
|
+
await selection.selectOption({index});
|
|
1126
|
+
optionText = optionTexts[index];
|
|
1407
1127
|
}
|
|
1408
1128
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1129
|
+
act.result.success = true;
|
|
1130
|
+
act.result.move = 'selected';
|
|
1131
|
+
act.result.option = optionText;
|
|
1132
|
+
}
|
|
1133
|
+
// Otherwise, if it is entering text in an input element:
|
|
1134
|
+
else if (['text', 'search'].includes(act.type)) {
|
|
1135
|
+
act.result.attributes = {};
|
|
1136
|
+
const {attributes} = act.result;
|
|
1137
|
+
const type = await selection.getAttribute('type');
|
|
1138
|
+
const label = await selection.getAttribute('aria-label');
|
|
1139
|
+
const labelRefs = await selection.getAttribute('aria-labelledby');
|
|
1140
|
+
attributes.type = type || '';
|
|
1141
|
+
attributes.label = label || '';
|
|
1142
|
+
attributes.labelRefs = labelRefs || '';
|
|
1143
|
+
// If the text contains a placeholder for an environment variable:
|
|
1144
|
+
let {what} = act;
|
|
1145
|
+
if (/__[A-Z]+__/.test(what)) {
|
|
1146
|
+
// Replace it.
|
|
1147
|
+
const envKey = /__([A-Z]+)__/.exec(what)[1];
|
|
1148
|
+
const envValue = process.env[envKey];
|
|
1149
|
+
what = what.replace(/__[A-Z]+__/, envValue);
|
|
1150
|
+
}
|
|
1151
|
+
// Enter the text.
|
|
1152
|
+
await selection.type(what);
|
|
1153
|
+
report.jobData.presses += what.length;
|
|
1154
|
+
act.result.success = true;
|
|
1155
|
+
act.result.move = 'entered';
|
|
1156
|
+
// If the input is a search input:
|
|
1157
|
+
if (act.type === 'search') {
|
|
1158
|
+
// Press the Enter key and wait for a network to be idle.
|
|
1159
|
+
doAndWait(false);
|
|
1416
1160
|
}
|
|
1417
1161
|
}
|
|
1418
|
-
// Otherwise, i.e. if
|
|
1162
|
+
// Otherwise, i.e. if the move is unknown, add the failure to the act.
|
|
1419
1163
|
else {
|
|
1420
|
-
//
|
|
1164
|
+
// Report the error.
|
|
1165
|
+
const report = 'ERROR: move unknown';
|
|
1421
1166
|
act.result.success = false;
|
|
1422
|
-
act.result.error =
|
|
1423
|
-
|
|
1424
|
-
console.log('ERROR: Specified element not found');
|
|
1425
|
-
await abortActs();
|
|
1167
|
+
act.result.error = report;
|
|
1168
|
+
console.log(report);
|
|
1426
1169
|
}
|
|
1427
1170
|
}
|
|
1428
|
-
// Otherwise, if
|
|
1429
|
-
else
|
|
1430
|
-
//
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
await page.keyboard.press(key);
|
|
1437
|
-
}
|
|
1438
|
-
const qualifier = act.again ? `${1 + act.again} times` : 'once';
|
|
1439
|
-
act.result = {
|
|
1440
|
-
success: true,
|
|
1441
|
-
message: `pressed ${qualifier}`
|
|
1442
|
-
};
|
|
1171
|
+
// Otherwise, i.e. if no match was found:
|
|
1172
|
+
else {
|
|
1173
|
+
// Quit and add failure data to the report.
|
|
1174
|
+
act.result.success = false;
|
|
1175
|
+
act.result.error = 'absent';
|
|
1176
|
+
act.result.message = 'ERROR: specified element not found';
|
|
1177
|
+
console.log('ERROR: Specified element not found');
|
|
1178
|
+
actIndex = await abortActs(report, actIndex);
|
|
1443
1179
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1180
|
+
}
|
|
1181
|
+
// Otherwise, if the act is a keypress:
|
|
1182
|
+
else if (act.type === 'press') {
|
|
1183
|
+
// Identify the number of times to press the key.
|
|
1184
|
+
let times = 1 + (act.again || 0);
|
|
1185
|
+
report.jobData.presses += times;
|
|
1186
|
+
const key = act.which;
|
|
1187
|
+
// Press the key.
|
|
1188
|
+
while (times--) {
|
|
1189
|
+
await page.keyboard.press(key);
|
|
1190
|
+
}
|
|
1191
|
+
const qualifier = act.again ? `${1 + act.again} times` : 'once';
|
|
1192
|
+
act.result = {
|
|
1193
|
+
success: true,
|
|
1194
|
+
message: `pressed ${qualifier}`
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
// Otherwise, if it is a repetitive keyboard navigation:
|
|
1198
|
+
else if (act.type === 'presses') {
|
|
1199
|
+
const {navKey, what, which, withItems} = act;
|
|
1200
|
+
const matchTexts = which ? which.map(text => debloat(text)) : [];
|
|
1201
|
+
// Initialize the loop variables.
|
|
1202
|
+
let status = 'more';
|
|
1203
|
+
let presses = 0;
|
|
1204
|
+
let amountRead = 0;
|
|
1205
|
+
let items = [];
|
|
1206
|
+
let matchedText;
|
|
1207
|
+
// As long as a matching element has not been reached:
|
|
1208
|
+
while (status === 'more') {
|
|
1209
|
+
// Press the Escape key to dismiss any modal dialog.
|
|
1210
|
+
await page.keyboard.press('Escape');
|
|
1211
|
+
// Press the specified navigation key.
|
|
1212
|
+
await page.keyboard.press(navKey);
|
|
1213
|
+
presses++;
|
|
1214
|
+
// Identify the newly current element or a failure.
|
|
1215
|
+
const currentJSHandle = await page.evaluateHandle(actCount => {
|
|
1216
|
+
// Initialize it as the focused element.
|
|
1217
|
+
let currentElement = document.activeElement;
|
|
1218
|
+
// If it exists in the page:
|
|
1219
|
+
if (currentElement && currentElement.tagName !== 'BODY') {
|
|
1220
|
+
// Change it, if necessary, to its active descendant.
|
|
1221
|
+
if (currentElement.hasAttribute('aria-activedescendant')) {
|
|
1222
|
+
currentElement = document.getElementById(
|
|
1223
|
+
currentElement.getAttribute('aria-activedescendant')
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
// Or change it, if necessary, to its selected option.
|
|
1227
|
+
else if (currentElement.tagName === 'SELECT') {
|
|
1228
|
+
const currentIndex = Math.max(0, currentElement.selectedIndex);
|
|
1229
|
+
const options = currentElement.querySelectorAll('option');
|
|
1230
|
+
currentElement = options[currentIndex];
|
|
1231
|
+
}
|
|
1232
|
+
// Or change it, if necessary, to its active shadow-DOM element.
|
|
1233
|
+
else if (currentElement.shadowRoot) {
|
|
1234
|
+
currentElement = currentElement.shadowRoot.activeElement;
|
|
1235
|
+
}
|
|
1236
|
+
// If there is a current element:
|
|
1237
|
+
if (currentElement) {
|
|
1238
|
+
// If it was already reached within this act:
|
|
1239
|
+
if (currentElement.dataset.pressesReached === actCount.toString(10)) {
|
|
1501
1240
|
// Report the error.
|
|
1241
|
+
console.log(`ERROR: ${currentElement.tagName} element reached again`);
|
|
1502
1242
|
status = 'ERROR';
|
|
1503
|
-
return '
|
|
1243
|
+
return 'ERROR: locallyExhausted';
|
|
1244
|
+
}
|
|
1245
|
+
// Otherwise, i.e. if it is newly reached within this act:
|
|
1246
|
+
else {
|
|
1247
|
+
// Mark and return it.
|
|
1248
|
+
currentElement.dataset.pressesReached = actCount;
|
|
1249
|
+
return currentElement;
|
|
1504
1250
|
}
|
|
1505
1251
|
}
|
|
1506
|
-
// Otherwise, i.e. if there is no
|
|
1252
|
+
// Otherwise, i.e. if there is no current element:
|
|
1507
1253
|
else {
|
|
1508
1254
|
// Report the error.
|
|
1509
1255
|
status = 'ERROR';
|
|
1510
|
-
return '
|
|
1256
|
+
return 'noActiveElement';
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
// Otherwise, i.e. if there is no focus in the page:
|
|
1260
|
+
else {
|
|
1261
|
+
// Report the error.
|
|
1262
|
+
status = 'ERROR';
|
|
1263
|
+
return 'ERROR: globallyExhausted';
|
|
1264
|
+
}
|
|
1265
|
+
}, actCount);
|
|
1266
|
+
// If the current element exists:
|
|
1267
|
+
const currentElement = currentJSHandle.asElement();
|
|
1268
|
+
if (currentElement) {
|
|
1269
|
+
// Update the data.
|
|
1270
|
+
const tagNameJSHandle = await currentElement.getProperty('tagName');
|
|
1271
|
+
const tagName = await tagNameJSHandle.jsonValue();
|
|
1272
|
+
const text = await textOf(page, currentElement);
|
|
1273
|
+
// If the text of the current element was found:
|
|
1274
|
+
if (text !== null) {
|
|
1275
|
+
const textLength = text.length;
|
|
1276
|
+
// If it is non-empty and there are texts to match:
|
|
1277
|
+
if (matchTexts.length && textLength) {
|
|
1278
|
+
// Identify the matching text.
|
|
1279
|
+
matchedText = matchTexts.find(matchText => text.includes(matchText));
|
|
1511
1280
|
}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (text !== null) {
|
|
1522
|
-
const textLength = text.length;
|
|
1523
|
-
// If it is non-empty and there are texts to match:
|
|
1524
|
-
if (matchTexts.length && textLength) {
|
|
1525
|
-
// Identify the matching text.
|
|
1526
|
-
matchedText = matchTexts.find(matchText => text.includes(matchText));
|
|
1281
|
+
// Update the item data if required.
|
|
1282
|
+
if (withItems) {
|
|
1283
|
+
const itemData = {
|
|
1284
|
+
tagName,
|
|
1285
|
+
text,
|
|
1286
|
+
textLength
|
|
1287
|
+
};
|
|
1288
|
+
if (matchedText) {
|
|
1289
|
+
itemData.matchedText = matchedText;
|
|
1527
1290
|
}
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1291
|
+
items.push(itemData);
|
|
1292
|
+
}
|
|
1293
|
+
amountRead += textLength;
|
|
1294
|
+
// If there is no text-match failure:
|
|
1295
|
+
if (matchedText || ! matchTexts.length) {
|
|
1296
|
+
// If the element has any specified tag name:
|
|
1297
|
+
if (! what || tagName === what) {
|
|
1298
|
+
// Change the status.
|
|
1299
|
+
status = 'done';
|
|
1300
|
+
// Perform the action.
|
|
1301
|
+
const inputText = act.text;
|
|
1302
|
+
if (inputText) {
|
|
1303
|
+
await page.keyboard.type(inputText);
|
|
1304
|
+
presses += inputText.length;
|
|
1537
1305
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
if (matchedText || ! matchTexts.length) {
|
|
1543
|
-
// If the element has any specified tag name:
|
|
1544
|
-
if (! what || tagName === what) {
|
|
1545
|
-
// Change the status.
|
|
1546
|
-
status = 'done';
|
|
1547
|
-
// Perform the action.
|
|
1548
|
-
const inputText = act.text;
|
|
1549
|
-
if (inputText) {
|
|
1550
|
-
await page.keyboard.type(inputText);
|
|
1551
|
-
presses += inputText.length;
|
|
1552
|
-
}
|
|
1553
|
-
if (act.action) {
|
|
1554
|
-
presses++;
|
|
1555
|
-
await page.keyboard.press(act.action);
|
|
1556
|
-
await page.waitForLoadState();
|
|
1557
|
-
}
|
|
1306
|
+
if (act.action) {
|
|
1307
|
+
presses++;
|
|
1308
|
+
await page.keyboard.press(act.action);
|
|
1309
|
+
await page.waitForLoadState();
|
|
1558
1310
|
}
|
|
1559
1311
|
}
|
|
1560
1312
|
}
|
|
1561
|
-
else {
|
|
1562
|
-
status = 'ERROR';
|
|
1563
|
-
}
|
|
1564
1313
|
}
|
|
1565
|
-
// Otherwise, i.e. if there was a failure:
|
|
1566
1314
|
else {
|
|
1567
|
-
|
|
1568
|
-
status = await currentJSHandle.jsonValue();
|
|
1315
|
+
status = 'ERROR';
|
|
1569
1316
|
}
|
|
1570
1317
|
}
|
|
1571
|
-
//
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
status
|
|
1575
|
-
totals: {
|
|
1576
|
-
presses,
|
|
1577
|
-
amountRead
|
|
1578
|
-
}
|
|
1579
|
-
};
|
|
1580
|
-
if (status === 'done' && matchedText) {
|
|
1581
|
-
act.result.matchedText = matchedText;
|
|
1318
|
+
// Otherwise, i.e. if there was a failure:
|
|
1319
|
+
else {
|
|
1320
|
+
// Update the status.
|
|
1321
|
+
status = await currentJSHandle.jsonValue();
|
|
1582
1322
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
1323
|
+
}
|
|
1324
|
+
// Add the result to the act.
|
|
1325
|
+
act.result = {
|
|
1326
|
+
success: true,
|
|
1327
|
+
status,
|
|
1328
|
+
totals: {
|
|
1329
|
+
presses,
|
|
1330
|
+
amountRead
|
|
1585
1331
|
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1332
|
+
};
|
|
1333
|
+
if (status === 'done' && matchedText) {
|
|
1334
|
+
act.result.matchedText = matchedText;
|
|
1589
1335
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
// Add the error result to the act and abort the job.
|
|
1593
|
-
actIndex = await addError(true, true, report, actIndex, 'ERROR: Invalid act type');
|
|
1336
|
+
if (withItems) {
|
|
1337
|
+
act.result.items = items;
|
|
1594
1338
|
}
|
|
1339
|
+
// Add the totals to the report.
|
|
1340
|
+
report.jobData.presses += presses;
|
|
1341
|
+
report.jobData.amountRead += amountRead;
|
|
1595
1342
|
}
|
|
1596
|
-
// Otherwise,
|
|
1343
|
+
// Otherwise, i.e. if the act type is unknown:
|
|
1597
1344
|
else {
|
|
1598
|
-
// Add
|
|
1599
|
-
actIndex = await addError(true, true, report, actIndex, 'ERROR:
|
|
1345
|
+
// Add the error result to the act and abort the job.
|
|
1346
|
+
actIndex = await addError(true, true, report, actIndex, 'ERROR: Invalid act type');
|
|
1600
1347
|
}
|
|
1601
1348
|
}
|
|
1602
|
-
// Otherwise,
|
|
1349
|
+
// Otherwise, a page URL is required but does not exist, so:
|
|
1603
1350
|
else {
|
|
1604
1351
|
// Add an error result to the act and abort the job.
|
|
1605
|
-
actIndex = await addError(true, true, report, actIndex, 'ERROR:
|
|
1352
|
+
actIndex = await addError(true, true, report, actIndex, 'ERROR: Page has no URL');
|
|
1606
1353
|
}
|
|
1607
|
-
act.endTime = Date.now();
|
|
1608
1354
|
}
|
|
1609
|
-
// Otherwise, i.e. if
|
|
1355
|
+
// Otherwise, i.e. if no page exists:
|
|
1610
1356
|
else {
|
|
1611
|
-
// Add error
|
|
1612
|
-
addError(true, true, report, actIndex,
|
|
1357
|
+
// Add an error result to the act and abort the job.
|
|
1358
|
+
actIndex = await addError(true, true, report, actIndex, 'ERROR: No page identified');
|
|
1613
1359
|
}
|
|
1360
|
+
act.endTime = Date.now();
|
|
1614
1361
|
// Perform any remaining acts if not aborted.
|
|
1615
1362
|
await doActs(report, actIndex + 1, page);
|
|
1616
1363
|
}
|
|
@@ -1628,7 +1375,7 @@ exports.doJob = async report => {
|
|
|
1628
1375
|
// If the report is valid:
|
|
1629
1376
|
report.jobData = {};
|
|
1630
1377
|
const {jobData} = report;
|
|
1631
|
-
const reportInvalidity =
|
|
1378
|
+
const reportInvalidity = isValidJob(report);
|
|
1632
1379
|
if (reportInvalidity) {
|
|
1633
1380
|
console.log(`ERROR: ${reportInvalidity}`);
|
|
1634
1381
|
jobData.aborted = true;
|