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/run.js CHANGED
@@ -25,20 +25,22 @@
25
25
  Testaro main utility module.
26
26
  */
27
27
 
28
- // ########## IMPORTS
28
+ // IMPORTS
29
29
 
30
30
  // Module to keep secrets.
31
31
  require('dotenv').config();
32
- // Requirements for acts.
33
- const {actSpecs} = require('./actSpecs');
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
- // ########## CONSTANTS
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
- // ########## VALIDATORS
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
- report, typeName, url, debug, waits, deviceID = 'default', device, motion = 'no-preference'
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
- const browserType = require('playwright')[typeName];
417
- if (browserType !== 'default') {
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
- // Launch a browser of the specified type.
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
- if (debug) {
428
- browserOptions.headless = false;
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
- // If a non-default device was specified:
444
- let options = {
445
- reduceMotion: motion
446
- };
447
- if (deviceID && deviceID !== 'default') {
448
- // Get its properties.
449
- const {devices} = require('playwright');
450
- const deviceProperties = devices[deviceID];
451
- if (deviceProperties) {
452
- options = {
453
- ... options,
454
- ... deviceProperties
455
- };
456
- }
457
- else {
458
- console.log(`ERROR: Device ${deviceID} does not exist`);
459
- }
460
- }
461
- // Open a context (i.e. browser tab), with reduced motion if specified.
462
- const browserContext = await browser.newContext(options);
463
- // Prevent default timeouts.
464
- browserContext.setDefaultTimeout(0);
465
- // When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
466
- browserContext.on('page', async page => {
467
- // Ensure the report has a jobData property.
468
- report.jobData ??= {};
469
- report.jobData.logCount ??= 0;
470
- report.jobData.logSize ??= 0;
471
- report.jobData.errorLogCount ??= 0;
472
- report.jobData.deviceID ??= deviceID;
473
- report.jobData.browserTabOptions ??= options;
474
- // Add any error events to the count of logging errors.
475
- page.on('crash', () => {
476
- report.jobData.errorLogCount++;
477
- console.log('Page crashed');
478
- });
479
- page.on('pageerror', () => {
480
- report.jobData.errorLogCount++;
481
- });
482
- page.on('requestfailed', () => {
483
- report.jobData.errorLogCount++;
484
- });
485
- // If the page emits a message:
486
- page.on('console', msg => {
487
- const msgText = msg.text();
488
- let indentedMsg = '';
489
- // If debugging is on:
490
- if (debug) {
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
- indentedMsg = parts.map(part => ` | ${part}`).join('\n');
507
- console.log(`\n${indentedMsg}`);
508
- }
509
- // Add statistics on the message to the report.
510
- const msgTextLC = msgText.toLowerCase();
511
- const msgLength = msgText.length;
512
- report.jobData.logCount++;
513
- report.jobData.logSize += msgLength;
514
- if (errorWords.some(word => msgTextLC.includes(word))) {
515
- report.jobData.errorLogCount++;
516
- report.jobData.errorLogSize += msgLength;
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
- const msgLC = msgText.toLowerCase();
519
- if (
520
- msgText.includes('403') && (msgLC.includes('status')
521
- || msgLC.includes('prohibited'))
522
- ) {
523
- report.jobData.prohibitedCount++;
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
- // Otherwise, if the navigation failed:
547
- else {
548
- // Return the error.
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: navResult.error
326
+ error: 'Blank page load in new tab timed out'
552
327
  };
553
328
  }
554
329
  }
555
- // If it fails to become stable after load:
556
- catch(error) {
330
+ // Otherwise, i.e. if the device is invalid:
331
+ else {
557
332
  // Return this.
558
- console.log(`ERROR: Blank page load in new tab timed out (${error.message})`);
333
+ console.log(`ERROR: Device ${deviceID} invalid`);
559
334
  return {
560
335
  success: false,
561
- error: 'Blank page load in new tab timed out'
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 ${typeName} could not be launched`);
343
+ console.log(`ERROR: Browser of type ${browserID} could not be launched`);
569
344
  return {
570
345
  success: false,
571
- error: `${typeName} browser launch failed`
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 being aborted.
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
- // If it is valid:
805
- if (isValidAct(act)) {
806
- let actInfo = '';
807
- if (act.which) {
808
- if (act.type === 'launch' && act.url) {
809
- actInfo = `${act.which} to ${act.url}`;
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
- else {
812
- actInfo = act.which;
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
- const message = `>>>> ${act.type}: ${actInfo}`;
816
- // If granular reporting has been specified:
817
- if (report.observe) {
818
- // Notify the observer of the act and log it.
819
- const whichParam = act.which ? `&which=${act.which}` : '';
820
- const messageParams = `act=${act.type}${whichParam}`;
821
- tellServer(report, messageParams, message);
822
- }
823
- // Otherwise, i.e. if granular reporting has not been specified:
824
- else {
825
- // Log the act.
826
- console.log(message);
827
- }
828
- // Increment the count of acts performed.
829
- actCount++;
830
- act.startTime = Date.now();
831
- // If the act is an index changer:
832
- if (act.type === 'next') {
833
- const condition = act.if;
834
- const logSuffix = condition.length === 3 ? ` ${condition[1]} ${condition[2]}` : '';
835
- console.log(`>> ${condition[0]}${logSuffix}`);
836
- // Identify the act to be checked.
837
- const ifActIndex = report.acts.map(act => act.type !== 'next').lastIndexOf(true);
838
- // Determine whether its jump condition is true.
839
- const truth = isTrue(report.acts[ifActIndex].result, condition);
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 act is a launch:
868
- else if (act.type === 'launch') {
869
- // Launch the specified browser and navigate to the specified URL.
870
- const launchResult = await launch(
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
- // If the launch and navigation succeeded:
880
- if (launchResult && launchResult.success) {
881
- // Get the response of the target server.
882
- const {response} = launchResult;
883
- // Get the target page.
884
- page = launchResult.page;
885
- // Add the actual URL to the act.
886
- act.actualURL = page.url();
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 scriptNonce = await getNonce(response);
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 launch or navigation failed:
684
+ // Otherwise, i.e. if the visit failed:
894
685
  else {
895
- // Add an error result to the act and abort the job.
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 current page exists:
902
- else if (page) {
903
- // If the act is navigation to a url:
904
- if (act.type === 'url') {
905
- // Identify the URL.
906
- const resolved = act.which.replace('__dirname', __dirname);
907
- requestedURL = resolved;
908
- // Visit it and wait until the DOM is loaded.
909
- const navResult = await goTo(report, page, requestedURL, 15000, 'domcontentloaded');
910
- // If the visit succeeded:
911
- if (navResult.success) {
912
- // Add the script nonce, if any, to the act.
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
- // Otherwise, i.e. if the visit failed:
932
- else {
933
- // Report this and abort the job.
934
- actIndex = await addError(true, true, report, actIndex, 'ERROR: Visit failed');
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 act is a wait for text:
938
- else if (act.type === 'wait') {
939
- const {what, which} = act;
940
- console.log(`>> ${what}`);
941
- const result = act.result = {};
942
- // If the text is to be the URL:
943
- if (what === 'url') {
944
- // Wait for the URL to be the exact text.
945
- try {
946
- await page.waitForURL(which, {timeout: 15000});
947
- result.found = true;
948
- result.url = page.url();
949
- }
950
- // If the wait times out:
951
- catch(error) {
952
- // Quit.
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
- // Otherwise, if the text is to be a substring of the text of the page body:
982
- else if (what === 'body') {
983
- // Wait for the body to include the text, case-insensitively.
984
- try {
985
- await page.waitForFunction(
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 act is a wait for a state:
1006
- else if (act.type === 'state') {
1007
- // Wait for it.
1008
- const stateIndex = ['loaded', 'idle'].indexOf(act.which);
1009
- await page.waitForLoadState(
1010
- ['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 15000][stateIndex]}
1011
- )
1012
- // If the wait times out:
1013
- .catch(async error => {
1014
- // Report this and abort the job.
1015
- console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
1016
- actIndex = await addError(
1017
- true, true, report, actIndex, `ERROR waiting for page to be ${act.which}`
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
- // If the wait succeeded:
1021
- if (actIndex > -2) {
1022
- // Add state data to the report.
1023
- act.result = {
1024
- success: true,
1025
- state: act.which
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
- // Otherwise, if the act is a page switch:
1030
- else if (act.type === 'page') {
1031
- // Wait for a page to be created and identify it as current.
1032
- page = await browserContext.waitForEvent('page');
1033
- // Wait until it is idle.
1034
- await page.waitForLoadState('networkidle', {timeout: 15000});
1035
- // Add the resulting URL to the act.
1036
- const result = {
1037
- url: page.url()
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
- // Otherwise, if the page has a URL:
1042
- else if (page.url() && page.url() !== 'about:blank') {
1043
- const url = page.url();
1044
- // Add the URL to the act.
1045
- act.actualURL = url;
1046
- // If the act is a revelation:
1047
- if (act.type === 'reveal') {
1048
- // Make all elements in the page visible.
1049
- await page.$$eval('body *', elements => {
1050
- elements.forEach(element => {
1051
- const styleDec = window.getComputedStyle(element);
1052
- if (styleDec.display === 'none') {
1053
- element.style.display = 'initial';
1054
- }
1055
- if (['hidden', 'collapse'].includes(styleDec.visibility)) {
1056
- element.style.visibility = 'inherit';
1057
- }
1058
- });
1059
- act.result = {
1060
- success: true
1061
- };
1062
- })
1063
- .catch(error => {
1064
- console.log(`ERROR making all elements visible (${error.message})`);
1065
- act.result = {
1066
- success: false
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
- // Get the start time of the act.
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
- // If the testing failed:
1100
- catch(error) {
1101
- // Report this.
1102
- const message = error.message.slice(0, 400);
1103
- console.log(`ERROR: Test act ${act.which} failed (${message})`);
1104
- act.data.error = act.data.error ? `${act.data.error}; ${message}` : message;
1105
- }
1106
- // Add the elapsed time of the tool to the report.
1107
- const time = Math.round((Date.now() - startTime) / 1000);
1108
- const {toolTimes} = report.jobData;
1109
- if (! toolTimes[act.which]) {
1110
- toolTimes[act.which] = 0;
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
- toolTimes[act.which] += time;
1113
- // If a standard-format result is to be included in the report:
1114
- const standard = report.standard || 'only';
1115
- if (['also', 'only'].includes(standard)) {
1116
- // Initialize it.
1117
- act.standardResult = {
1118
- totals: [0, 0, 0, 0],
1119
- instances: []
1120
- };
1121
- // Populate it.
1122
- standardize(act);
1123
- // Add a box ID and a path ID to each of its standard instances if missing.
1124
- for (const instance of act.standardResult.instances) {
1125
- const elementID = await identify(instance, page);
1126
- if (! instance.boxID) {
1127
- instance.boxID = elementID ? elementID.boxID : '';
1128
- }
1129
- if (! instance.pathID) {
1130
- instance.pathID = elementID ? elementID.pathID : '';
1131
- }
1132
- };
1133
- // If the original-format result is not to be included in the report:
1134
- if (standard === 'only') {
1135
- // Remove it.
1136
- delete act.result;
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
- // If the act has expectations:
1140
- const expectations = act.expect;
1141
- if (expectations) {
1142
- // Initialize whether they were fulfilled.
1143
- act.expectations = [];
1144
- let failureCount = 0;
1145
- // For each expectation:
1146
- expectations.forEach(spec => {
1147
- const truth = isTrue(act, spec);
1148
- act.expectations.push({
1149
- property: spec[0],
1150
- relation: spec[1],
1151
- criterion: spec[2],
1152
- actual: truth[0],
1153
- passed: truth[1]
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
- act.expectationFailures = failureCount;
1160
- }
908
+ if (! truth[1]) {
909
+ failureCount++;
910
+ }
911
+ });
912
+ act.expectationFailures = failureCount;
1161
913
  }
1162
- // Otherwise, if the act is a move:
1163
- else if (moves[act.type]) {
1164
- const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
1165
- // Try up to 5 times to:
1166
- act.result = {found: false};
1167
- let selection = {};
1168
- let tries = 0;
1169
- const slimText = act.which ? debloat(act.which) : '';
1170
- while (tries++ < 5 && ! act.result.found) {
1171
- if (page) {
1172
- // Identify the elements of the specified type.
1173
- const selections = await page.$$(selector);
1174
- // If there are any:
1175
- if (selections.length) {
1176
- // If there are enough to make a match possible:
1177
- if ((act.index || 0) < selections.length) {
1178
- // For each element of the specified type:
1179
- let matchCount = 0;
1180
- const selectionTexts = [];
1181
- for (selection of selections) {
1182
- // Add its lower-case text or an empty string to the list of element texts.
1183
- const selectionText = slimText ? await textOf(page, selection) : '';
1184
- selectionTexts.push(selectionText);
1185
- // If its text includes any specified text, case-insensitively:
1186
- if (selectionText.includes(slimText)) {
1187
- // If the element has the specified index among such elements:
1188
- if (matchCount++ === (act.index || 0)) {
1189
- // Report it as the matching element and stop checking.
1190
- act.result.found = true;
1191
- act.result.textSpec = slimText;
1192
- act.result.textContent = selectionText;
1193
- break;
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
- // Otherwise, i.e. if there are too few such elements to make a match possible:
1211
- else {
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 = 'fewer';
954
+ act.result.error = 'exhausted';
1215
955
  act.result.typeElementCount = selections.length;
1216
- act.result.message = 'Elements of specified type too few';
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 no elements of the specified type:
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 = 'none';
1224
- act.result.typeElementCount = 0;
1225
- act.result.message = 'No elements of specified type found';
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 the page no longer exists:
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 = 'gone';
1233
- act.result.message = 'Page gone';
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
- // If a match was found:
1240
- if (act.result.found) {
1241
- // FUNCTION DEFINITION START
1242
- // Performs a click or Enter keypress and waits for the network to be idle.
1243
- const doAndWait = async isClick => {
1244
- // Perform and report the move.
1245
- const move = isClick ? 'click' : 'Enter keypress';
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 isClick
1248
- ? selection.click({timeout: 4000})
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
- // Add the error result to the act and abort the job.
1256
- actIndex = await addError(true, true, report, actIndex, `ERROR: ${move} failed`);
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
- // FUNCTION DEFINITION END
1273
- // If the move is a button click, perform it.
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
- // Otherwise, if it is checking a radio button or checkbox, perform it.
1280
- else if (['checkbox', 'radio'].includes(act.type)) {
1281
- await selection.waitForElementState('stable', {timeout: 2000})
1282
- .catch(error => {
1283
- console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
1284
- act.result.success = false;
1285
- act.result.error = `ERROR waiting for stable ${act.type}`;
1286
- });
1287
- if (! act.result.error) {
1288
- const isEnabled = await selection.isEnabled();
1289
- if (isEnabled) {
1290
- await selection.check({
1291
- force: true,
1292
- timeout: 2000
1293
- })
1294
- .catch(error => {
1295
- console.log(`ERROR checking ${act.type} (${error.message})`);
1296
- act.result.success = false;
1297
- act.result.error = `ERROR checking ${act.type}`;
1298
- });
1299
- if (! act.result.error) {
1300
- act.result.success = true;
1301
- act.result.move = 'checked';
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 = report;
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
- // Otherwise, if it is focusing the element, perform it.
1313
- else if (act.type === 'focus') {
1314
- await selection.focus({timeout: 2000});
1315
- act.result.success = true;
1316
- act.result.move = 'focused';
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 it is clicking a link:
1319
- else if (act.type === 'link') {
1320
- const href = await selection.getAttribute('href');
1321
- const target = await selection.getAttribute('target');
1322
- act.result.href = href || 'NONE';
1323
- act.result.target = target || 'DEFAULT';
1324
- // If the destination is a new page:
1325
- if (target && target !== '_self') {
1326
- // Click the link and wait for the network to be idle.
1327
- doAndWait(true);
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
- // Otherwise, i.e. if the destination is in the current page:
1330
- else {
1331
- // Click the link and wait for the resulting navigation.
1332
- try {
1333
- await selection.click({timeout: 5000});
1334
- // Wait for the new content to load.
1335
- await page.waitForLoadState('domcontentloaded', {timeout: 6000});
1336
- act.result.success = true;
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
- // Otherwise, if it is selecting an option in a select list, perform it.
1358
- else if (act.type === 'select') {
1359
- const options = await selection.$$('option');
1360
- let optionText = '';
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
- // Otherwise, if it is entering text in an input element:
1381
- else if (['text', 'search'].includes(act.type)) {
1382
- act.result.attributes = {};
1383
- const {attributes} = act.result;
1384
- const type = await selection.getAttribute('type');
1385
- const label = await selection.getAttribute('aria-label');
1386
- const labelRefs = await selection.getAttribute('aria-labelledby');
1387
- attributes.type = type || '';
1388
- attributes.label = label || '';
1389
- attributes.labelRefs = labelRefs || '';
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
- // Enter the text.
1399
- await selection.type(what);
1400
- report.jobData.presses += what.length;
1401
- act.result.success = true;
1402
- act.result.move = 'entered';
1403
- // If the input is a search input:
1404
- if (act.type === 'search') {
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
- // Otherwise, i.e. if the move is unknown, add the failure to the act.
1410
- else {
1411
- // Report the error.
1412
- const report = 'ERROR: move unknown';
1413
- act.result.success = false;
1414
- act.result.error = report;
1415
- console.log(report);
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 no match was found:
1162
+ // Otherwise, i.e. if the move is unknown, add the failure to the act.
1419
1163
  else {
1420
- // Quit and add failure data to the report.
1164
+ // Report the error.
1165
+ const report = 'ERROR: move unknown';
1421
1166
  act.result.success = false;
1422
- act.result.error = 'absent';
1423
- act.result.message = 'ERROR: specified element not found';
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 the act is a keypress:
1429
- else if (act.type === 'press') {
1430
- // Identify the number of times to press the key.
1431
- let times = 1 + (act.again || 0);
1432
- report.jobData.presses += times;
1433
- const key = act.which;
1434
- // Press the key.
1435
- while (times--) {
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
- // Otherwise, if it is a repetitive keyboard navigation:
1445
- else if (act.type === 'presses') {
1446
- const {navKey, what, which, withItems} = act;
1447
- const matchTexts = which ? which.map(text => debloat(text)) : [];
1448
- // Initialize the loop variables.
1449
- let status = 'more';
1450
- let presses = 0;
1451
- let amountRead = 0;
1452
- let items = [];
1453
- let matchedText;
1454
- // As long as a matching element has not been reached:
1455
- while (status === 'more') {
1456
- // Press the Escape key to dismiss any modal dialog.
1457
- await page.keyboard.press('Escape');
1458
- // Press the specified navigation key.
1459
- await page.keyboard.press(navKey);
1460
- presses++;
1461
- // Identify the newly current element or a failure.
1462
- const currentJSHandle = await page.evaluateHandle(actCount => {
1463
- // Initialize it as the focused element.
1464
- let currentElement = document.activeElement;
1465
- // If it exists in the page:
1466
- if (currentElement && currentElement.tagName !== 'BODY') {
1467
- // Change it, if necessary, to its active descendant.
1468
- if (currentElement.hasAttribute('aria-activedescendant')) {
1469
- currentElement = document.getElementById(
1470
- currentElement.getAttribute('aria-activedescendant')
1471
- );
1472
- }
1473
- // Or change it, if necessary, to its selected option.
1474
- else if (currentElement.tagName === 'SELECT') {
1475
- const currentIndex = Math.max(0, currentElement.selectedIndex);
1476
- const options = currentElement.querySelectorAll('option');
1477
- currentElement = options[currentIndex];
1478
- }
1479
- // Or change it, if necessary, to its active shadow-DOM element.
1480
- else if (currentElement.shadowRoot) {
1481
- currentElement = currentElement.shadowRoot.activeElement;
1482
- }
1483
- // If there is a current element:
1484
- if (currentElement) {
1485
- // If it was already reached within this act:
1486
- if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1487
- // Report the error.
1488
- console.log(`ERROR: ${currentElement.tagName} element reached again`);
1489
- status = 'ERROR';
1490
- return 'ERROR: locallyExhausted';
1491
- }
1492
- // Otherwise, i.e. if it is newly reached within this act:
1493
- else {
1494
- // Mark and return it.
1495
- currentElement.dataset.pressesReached = actCount;
1496
- return currentElement;
1497
- }
1498
- }
1499
- // Otherwise, i.e. if there is no current element:
1500
- else {
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 'noActiveElement';
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 focus in the page:
1252
+ // Otherwise, i.e. if there is no current element:
1507
1253
  else {
1508
1254
  // Report the error.
1509
1255
  status = 'ERROR';
1510
- return 'ERROR: globallyExhausted';
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
- }, actCount);
1513
- // If the current element exists:
1514
- const currentElement = currentJSHandle.asElement();
1515
- if (currentElement) {
1516
- // Update the data.
1517
- const tagNameJSHandle = await currentElement.getProperty('tagName');
1518
- const tagName = await tagNameJSHandle.jsonValue();
1519
- const text = await textOf(page, currentElement);
1520
- // If the text of the current element was found:
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
- // Update the item data if required.
1529
- if (withItems) {
1530
- const itemData = {
1531
- tagName,
1532
- text,
1533
- textLength
1534
- };
1535
- if (matchedText) {
1536
- itemData.matchedText = matchedText;
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
- items.push(itemData);
1539
- }
1540
- amountRead += textLength;
1541
- // If there is no text-match failure:
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
- // Update the status.
1568
- status = await currentJSHandle.jsonValue();
1315
+ status = 'ERROR';
1569
1316
  }
1570
1317
  }
1571
- // Add the result to the act.
1572
- act.result = {
1573
- success: true,
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
- if (withItems) {
1584
- act.result.items = items;
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
- // Add the totals to the report.
1587
- report.jobData.presses += presses;
1588
- report.jobData.amountRead += amountRead;
1332
+ };
1333
+ if (status === 'done' && matchedText) {
1334
+ act.result.matchedText = matchedText;
1589
1335
  }
1590
- // Otherwise, i.e. if the act type is unknown:
1591
- else {
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, a page URL is required but does not exist, so:
1343
+ // Otherwise, i.e. if the act type is unknown:
1597
1344
  else {
1598
- // Add an error result to the act and abort the job.
1599
- actIndex = await addError(true, true, report, actIndex, 'ERROR: Page has no URL');
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, i.e. if no page exists:
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: No page identified');
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 the act is invalid:
1355
+ // Otherwise, i.e. if no page exists:
1610
1356
  else {
1611
- // Add error data to the act and abort the job.
1612
- addError(true, true, report, actIndex, `ERROR: Invalid act of type ${act.type}`);
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 = isValidReport(report);
1378
+ const reportInvalidity = isValidJob(report);
1632
1379
  if (reportInvalidity) {
1633
1380
  console.log(`ERROR: ${reportInvalidity}`);
1634
1381
  jobData.aborted = true;