testaro 40.0.2 → 41.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/actSpecs.js CHANGED
@@ -54,9 +54,9 @@ exports.actSpecs = {
54
54
  launch: [
55
55
  'Launch a Playwright browser',
56
56
  {
57
- which: [true, 'string', 'isBrowserType', 'chromium, firefox, or webkit'],
58
- url: [true, 'string', 'isURL', 'initial URL to navigate to'],
59
- deviceID: [false, 'string', '', 'Playwright device ID if not default, e.g. iPhone 6 landscape'],
57
+ url: [false, 'string', 'isURL', 'initial URL to navigate to'],
58
+ deviceID: [false, 'string', 'isDeviceID', 'Playwright device ID if not default, e.g. iPhone 6 landscape'],
59
+ browserID: [false, 'string', 'isBrowserID', 'chromium, firefox, or webkit if not job default'],
60
60
  lowMotion: [false, 'boolean', '', 'set reduced-motion option if true'],
61
61
  what: [false, 'string', 'hasLength', 'comment']
62
62
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "40.0.2",
3
+ "version": "41.0.0",
4
4
  "description": "Run 1000 web accessibility tests from 10 tools and get a standardized report",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,59 @@
1
+ /*
2
+ © 2024 CVS Health and/or one of its affiliates. All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
21
+ */
22
+
23
+ // device
24
+
25
+ // IMPORTS
26
+
27
+ const {devices} = require('playwright');
28
+
29
+ // FUNCTIONS
30
+
31
+ // Returns whether a device ID is valid.
32
+ exports.isDeviceID = deviceID => deviceID === 'default' || !! devices[deviceID];
33
+
34
+ // Returns options for the browser.newContext() function.
35
+ exports.getDeviceOptions = (deviceID, motion) => {
36
+ const options = {
37
+ reduceMotion: motion
38
+ };
39
+ // If a non-default device was specified:
40
+ if (deviceID && deviceID !== 'default') {
41
+ // Get its properties if it exists.
42
+ const deviceProperties = devices[deviceID];
43
+ // Return options or report the device as invalid.
44
+ if (deviceProperties) {
45
+ return {
46
+ ... options,
47
+ ... deviceProperties
48
+ };
49
+ }
50
+ else {
51
+ return {};
52
+ }
53
+ }
54
+ // Otherwise, i.e. if no non-default device was specified:
55
+ else {
56
+ // Return options.
57
+ return options;
58
+ }
59
+ };
@@ -40,6 +40,8 @@ const cap = rawString => {
40
40
  return '';
41
41
  }
42
42
  };
43
+ // Returns whether an id attribute value is valid without character escaping.
44
+ const isBadID = id => /[^-\w]|^\d|^--|^-\d/.test(id);
43
45
  // Returns the tag name and the value of an id attribute from a substring of HTML code.
44
46
  const getIdentifiers = code => {
45
47
  let tagName = '';
@@ -47,7 +49,7 @@ const getIdentifiers = code => {
47
49
  // If the substring includes the start tag of an element:
48
50
  if (code && typeof code === 'string' && code.length && /<\s*[a-zA-Z]/.test(code)) {
49
51
  // Get the first start tag in the substring.
50
- const startTag = code.replace(/^.*?<(?=[a-zA-Z])/s, '').replace(/>.*$/s, '').trim();
52
+ const startTag = code.replace(/^.*?<(?=[a-zA-Z])/s, '').replace(/[^a-zA-Z].*$/gs, '').trim();
51
53
  // If it exists:
52
54
  if (startTag && startTag.length) {
53
55
  // Get its tag name, upper-cased.
@@ -57,6 +59,11 @@ const getIdentifiers = code => {
57
59
  if (idArray && idArray.length === 2) {
58
60
  id = idArray[1];
59
61
  }
62
+ // If the id value is invalid without character escaping:
63
+ if (isBadID(id)) {
64
+ // Remove it.
65
+ id = '';
66
+ }
60
67
  }
61
68
  }
62
69
  return [tagName, id];
@@ -207,7 +214,7 @@ const doHTMLCS = (result, standardResult, severity) => {
207
214
  what,
208
215
  ordinalSeverity: ['Warning', '', '', 'Error'].indexOf(severity),
209
216
  tagName: tagName.toUpperCase(),
210
- id: id.slice(1),
217
+ id: isBadID(id.slice(1)) ? '' : id.slice(1),
211
218
  location: {
212
219
  doc: 'dom',
213
220
  type: '',
@@ -317,7 +324,7 @@ const doWAVE = (result, standardResult, categoryName) => {
317
324
  if (finalTerm.includes('#')) {
318
325
  const finalArray = finalTerm.split('#');
319
326
  tagName = finalArray[0].replace(/:.*/, '');
320
- id = finalArray[1];
327
+ id = isBadID(finalArray[1]) ? '' : finalArray[1];
321
328
  }
322
329
  else {
323
330
  tagName = finalTerm.replace(/:.*/, '');
@@ -450,14 +457,16 @@ const convert = (toolName, data, result, standardResult) => {
450
457
  if (! tagName && finalRuleID.endsWith('_svg')) {
451
458
  tagName = 'SVG';
452
459
  }
453
- const excerpt = ruleResult.element && ruleResult.element.html || '';
460
+ const excerpt = ruleResult.element && ruleResult.element.html.replace(/\s+/g, ' ')
461
+ || '';
454
462
  if (! tagName && /^<[a-z]+[ >]/.test(excerpt)) {
455
- tagName = excerpt.slice(1).replace(/[ >]+/, '').toUpperCase();
463
+ tagName = excerpt.slice(1).replace(/[ >].+/, '').toUpperCase();
456
464
  }
457
465
  const idDraft = excerpt && excerpt.replace(/^[^[>]+id="/, 'id=').replace(/".*$/, '');
458
- const id = idDraft && idDraft.length > 3 && idDraft.startsWith('id=')
466
+ const idFinal = idDraft && idDraft.length > 3 && idDraft.startsWith('id=')
459
467
  ? idDraft.slice(3)
460
468
  : '';
469
+ const id = idFinal === '' || isBadID(idFinal) ? '' : idFinal;
461
470
  const instance = {
462
471
  ruleID: finalRuleID,
463
472
  what,
@@ -513,7 +522,7 @@ const convert = (toolName, data, result, standardResult) => {
513
522
  what,
514
523
  ordinalSeverity: 0,
515
524
  tagName,
516
- id,
525
+ id: isBadID(id) ? '' : id,
517
526
  location: {
518
527
  doc: 'dom',
519
528
  type: 'box',
@@ -567,7 +576,6 @@ const convert = (toolName, data, result, standardResult) => {
567
576
  if (result.rawPage) {
568
577
  doNuVal(result, standardResult, 'rawPage');
569
578
  }
570
- const {instances} = standardResult;
571
579
  }
572
580
  // qualWeb
573
581
  else if (
package/run.js CHANGED
@@ -37,6 +37,8 @@ const {standardize} = require('./procs/standardize');
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
 
@@ -103,7 +105,7 @@ let requestedURL = '';
103
105
  // ########## VALIDATORS
104
106
 
105
107
  // Validates a browser type.
106
- const isBrowserType = type => ['chromium', 'firefox', 'webkit'].includes(type);
108
+ const isBrowserID = type => ['chromium', 'firefox', 'webkit'].includes(type);
107
109
  // Validates a load state.
108
110
  const isState = string => ['loaded', 'idle'].includes(string);
109
111
  // Validates a URL.
@@ -146,8 +148,11 @@ const hasSubtype = (variable, subtype) => {
146
148
  else if (subtype === 'isURL') {
147
149
  return isURL(variable);
148
150
  }
149
- else if (subtype === 'isBrowserType') {
150
- return isBrowserType(variable);
151
+ else if (subtype === 'isDeviceID') {
152
+ return isDeviceID(variable);
153
+ }
154
+ else if (subtype === 'isBrowserID') {
155
+ return isBrowserID(variable);
151
156
  }
152
157
  else if (subtype === 'isFocusable') {
153
158
  return isFocusable(variable);
@@ -179,7 +184,7 @@ const hasSubtype = (variable, subtype) => {
179
184
  return true;
180
185
  }
181
186
  };
182
- // Validates an act.
187
+ // Validates an act by reference to actSpecs.js.
183
188
  const isValidAct = act => {
184
189
  // Identify the type of the act.
185
190
  const type = act.type;
@@ -250,60 +255,78 @@ const dateOf = timeStamp => {
250
255
  const isValidReport = report => {
251
256
  if (report) {
252
257
  // Return whether the report is valid.
253
- const {id, what, strict, timeLimit, acts, sources, creationTimeStamp, timeStamp} = report;
258
+ const {
259
+ id,
260
+ strict,
261
+ isolate,
262
+ standard,
263
+ observe,
264
+ deviceID,
265
+ browserID,
266
+ lowMotion,
267
+ timeLimit,
268
+ creationTimeStamp,
269
+ executionTimeStamp,
270
+ sources,
271
+ acts
272
+ } = report;
254
273
  if (! id || typeof id !== 'string') {
255
274
  return 'Bad report ID';
256
275
  }
257
- if (! what || typeof what !== 'string') {
258
- return 'Bad report what';
259
- }
260
276
  if (typeof strict !== 'boolean') {
261
277
  return 'Bad report strict';
262
278
  }
263
- if (typeof timeLimit !== 'number' || timeLimit < 1) {
264
- return 'Bad report time limit';
279
+ if (typeof isolate !== 'boolean') {
280
+ return 'Bad report isolate';
265
281
  }
266
- if (! acts || ! Array.isArray(acts) || ! acts.length) {
267
- return 'Bad report acts';
282
+ if (! ['also', 'only', 'no'].includes(standard)) {
283
+ return 'Bad report standard';
268
284
  }
269
- if (! acts.every(act => act.type && typeof act.type === 'string')) {
270
- return 'Act with no type';
285
+ if (typeof observe !== 'boolean') {
286
+ return 'Bad report observe';
271
287
  }
272
- if (acts[0].type !== 'launch') {
273
- return 'First act type not launch';
288
+ if (! isDeviceID(deviceID)) {
289
+ return 'Bad report deviceID';
274
290
  }
275
- if (! ['chromium', 'webkit', 'firefox'].includes(acts[0].which)) {
276
- return 'Bad first act which';
291
+ if (! ['chromium', 'firefox', 'webkit'].includes(browserID)) {
292
+ return 'Bad report browserID';
277
293
  }
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';
294
+ if (typeof lowMotion !== 'boolean') {
295
+ return 'Bad report lowMotion';
292
296
  }
293
- const invalidAct = acts.find(act => ! isValidAct(act));
294
- if (invalidAct) {
295
- return `Invalid act:\n${JSON.stringify(invalidAct, null, 2)}`;
297
+ if (typeof timeLimit !== 'number' || timeLimit < 1) {
298
+ return 'Bad report timeLimit';
299
+ }
300
+ if (
301
+ ! (creationTimeStamp && typeof creationTimeStamp === 'string' && dateOf(creationTimeStamp))
302
+ ) {
303
+ return 'bad job creationTimeStamp';
304
+ }
305
+ if (
306
+ ! (executionTimeStamp && typeof executionTimeStamp === 'string') && dateOf(executionTimeStamp)
307
+ ) {
308
+ return 'bad report executionTimeStamp';
296
309
  }
297
- if (! sources || typeof sources !== 'object') {
310
+ if (
311
+ ! sources
312
+ || typeof sources !== 'object'
313
+ || ! ['script', 'batch', 'target'].every(key => sources[key])
314
+ || ! ['what', 'url'].every(key => sources.target[key])
315
+ ) {
298
316
  return 'Bad report sources';
299
317
  }
300
318
  if (
301
- ! (creationTimeStamp && typeof creationTimeStamp === 'string' && dateOf(creationTimeStamp))
319
+ ! acts
320
+ || ! Array.isArray(acts)
321
+ || acts.length < 2
322
+ || ! acts.every(act => act.type && typeof act.type === 'string')
323
+ || acts[0].type !== 'launch'
302
324
  ) {
303
- return 'bad job creation time stamp';
325
+ return 'Bad report acts';
304
326
  }
305
- if (! (timeStamp && typeof timeStamp === 'string')) {
306
- return 'bad report time stamp';
327
+ const invalidAct = acts.find(act => ! isValidAct(act));
328
+ if (invalidAct) {
329
+ return `Invalid act:\n${JSON.stringify(invalidAct, null, 2)}`;
307
330
  }
308
331
  return '';
309
332
  }
@@ -409,27 +432,28 @@ const browserClose = async () => {
409
432
  }
410
433
  };
411
434
  // 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
- ) => {
435
+ const launch = async (report, url, debug, waits, deviceID, browserID, lowMotion) => {
436
+ // Get the default arguments.
437
+ url ??= report.url;
438
+ deviceID ??= report.deviceID;
439
+ browserID ??= report.browserID;
440
+ lowMotion ??= report.lowMotion;
415
441
  // If the specified browser type exists:
416
- const browserType = require('playwright')[typeName];
417
- if (browserType !== 'default') {
442
+ if (! browserID || ['chromium', 'firefox', 'webkit'].includes(browserID)) {
443
+ // Create a browser of the specified or default type.
444
+ const browserType = require('playwright')[browserID || report.browserID];
418
445
  // Close the current browser, if any.
419
446
  await browserClose();
420
- // Launch a browser of the specified type.
447
+ // Define browser options.
421
448
  const browserOptions = {
422
449
  logger: {
423
450
  isEnabled: () => false,
424
451
  log: (name, severity, message) => console.log(message.slice(0, 100))
425
452
  }
426
453
  };
427
- if (debug) {
428
- browserOptions.headless = false;
429
- }
430
- if (waits) {
431
- browserOptions.slowMo = waits;
432
- }
454
+ browserOptions.headless = ! debug;
455
+ browserOptions.slowMo = waits || 0;
456
+ // Launch the browser.
433
457
  browser = await browserType.launch(browserOptions)
434
458
  // If the launch failed:
435
459
  .catch(async error => {
@@ -440,135 +464,132 @@ const launch = async (
440
464
  error: 'Browser launch failed'
441
465
  };
442
466
  });
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));
467
+ // Get the device options for a new context.
468
+ const deviceOptions = getDeviceOptions(
469
+ deviceID || 'default', lowMotion ? 'reduce-motion' : 'no-preference'
470
+ );
471
+ // If the device is valid:
472
+ if (deviceOptions) {
473
+ // Open a context (i.e. browser tab), with reduced motion if specified.
474
+ const browserContext = await browser.newContext(deviceOptions);
475
+ // Prevent default timeouts.
476
+ browserContext.setDefaultTimeout(0);
477
+ // When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
478
+ browserContext.on('page', async page => {
479
+ // Ensure the report has a jobData property.
480
+ report.jobData ??= {};
481
+ report.jobData.logCount ??= 0;
482
+ report.jobData.logSize ??= 0;
483
+ report.jobData.errorLogCount ??= 0;
484
+ report.jobData.browserTabOptions ??= deviceOptions;
485
+ // Add any error events to the count of logging errors.
486
+ page.on('crash', () => {
487
+ report.jobData.errorLogCount++;
488
+ console.log('Page crashed');
489
+ });
490
+ page.on('pageerror', () => {
491
+ report.jobData.errorLogCount++;
492
+ });
493
+ page.on('requestfailed', () => {
494
+ report.jobData.errorLogCount++;
495
+ });
496
+ // If the page emits a message:
497
+ page.on('console', msg => {
498
+ const msgText = msg.text();
499
+ let indentedMsg = '';
500
+ // If debugging is on:
501
+ if (debug) {
502
+ // Log a summary of the message on the console.
503
+ const parts = [msgText.slice(0, 75)];
504
+ if (msgText.length > 75) {
505
+ parts.push(msgText.slice(75, 150));
506
+ if (msgText.length > 150) {
507
+ const tail = msgText.slice(150).slice(-150);
508
+ if (msgText.length > 300) {
509
+ parts.push('...');
510
+ }
511
+ parts.push(tail.slice(0, 75));
512
+ if (tail.length > 75) {
513
+ parts.push(tail.slice(75));
514
+ }
503
515
  }
504
516
  }
517
+ indentedMsg = parts.map(part => ` | ${part}`).join('\n');
518
+ console.log(`\n${indentedMsg}`);
505
519
  }
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;
520
+ // Add statistics on the message to the report.
521
+ const msgTextLC = msgText.toLowerCase();
522
+ const msgLength = msgText.length;
523
+ report.jobData.logCount++;
524
+ report.jobData.logSize += msgLength;
525
+ if (errorWords.some(word => msgTextLC.includes(word))) {
526
+ report.jobData.errorLogCount++;
527
+ report.jobData.errorLogSize += msgLength;
528
+ }
529
+ const msgLC = msgText.toLowerCase();
530
+ if (
531
+ msgText.includes('403') && (msgLC.includes('status')
532
+ || msgLC.includes('prohibited'))
533
+ ) {
534
+ report.jobData.prohibitedCount++;
535
+ }
536
+ });
537
+ });
538
+ // Open the first page of the context.
539
+ const page = await browserContext.newPage();
540
+ try {
541
+ // Wait until it is stable.
542
+ await page.waitForLoadState('domcontentloaded', {timeout: 5000});
543
+ // Navigate to the specified URL.
544
+ const navResult = await goTo(report, page, url, 15000, 'domcontentloaded');
545
+ // If the navigation succeeded:
546
+ if (navResult.success) {
547
+ // Update the name of the current browser type and store it in the page.
548
+ page.browserTypeName = browserID;
549
+ // Return the response of the target server, the browser context, and the page.
550
+ return {
551
+ success: true,
552
+ response: navResult.response,
553
+ browserContext,
554
+ page
555
+ };
517
556
  }
518
- const msgLC = msgText.toLowerCase();
519
- if (
520
- msgText.includes('403') && (msgLC.includes('status')
521
- || msgLC.includes('prohibited'))
522
- ) {
523
- report.jobData.prohibitedCount++;
557
+ // Otherwise, if the navigation failed:
558
+ else {
559
+ // Return the error.
560
+ return {
561
+ success: false,
562
+ error: navResult.error
563
+ };
524
564
  }
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
565
  }
546
- // Otherwise, if the navigation failed:
547
- else {
548
- // Return the error.
566
+ // If it fails to become stable after load:
567
+ catch(error) {
568
+ // Return this.
569
+ console.log(`ERROR: Blank page load in new tab timed out (${error.message})`);
549
570
  return {
550
571
  success: false,
551
- error: navResult.error
572
+ error: 'Blank page load in new tab timed out'
552
573
  };
553
574
  }
554
575
  }
555
- // If it fails to become stable after load:
556
- catch(error) {
576
+ // Otherwise, i.e. if the device is invalid:
577
+ else {
557
578
  // Return this.
558
- console.log(`ERROR: Blank page load in new tab timed out (${error.message})`);
579
+ console.log(`ERROR: Device ${deviceID} invalid`);
559
580
  return {
560
581
  success: false,
561
- error: 'Blank page load in new tab timed out'
582
+ error: `${deviceID} device invalid`
562
583
  };
563
584
  }
564
585
  }
565
586
  // Otherwise, i.e. if it does not exist:
566
587
  else {
567
588
  // Return this.
568
- console.log(`ERROR: Browser of type ${typeName} could not be launched`);
589
+ console.log(`ERROR: Browser of type ${browserID} could not be launched`);
569
590
  return {
570
591
  success: false,
571
- error: `${typeName} browser launch failed`
592
+ error: `${browserID} browser launch failed`
572
593
  };
573
594
  }
574
595
  };
@@ -801,23 +822,16 @@ const doActs = async (report, actIndex, page) => {
801
822
  if (actIndex > -1 && actIndex < acts.length) {
802
823
  // Identify the act to be performed.
803
824
  const act = acts[actIndex];
825
+ const {type, which} = act;
804
826
  // If it is valid:
805
827
  if (isValidAct(act)) {
806
- let actInfo = '';
807
- if (act.which) {
808
- if (act.type === 'launch' && act.url) {
809
- actInfo = `${act.which} to ${act.url}`;
810
- }
811
- else {
812
- actInfo = act.which;
813
- }
814
- }
815
- const message = `>>>> ${act.type}: ${actInfo}`;
828
+ const actSuffix = type === 'test' ? ` ${which}` : '';
829
+ const message = `>>>> ${type}${actSuffix}`;
816
830
  // If granular reporting has been specified:
817
831
  if (report.observe) {
818
832
  // Notify the observer of the act and log it.
819
- const whichParam = act.which ? `&which=${act.which}` : '';
820
- const messageParams = `act=${act.type}${whichParam}`;
833
+ const whichParam = which ? `&which=${which}` : '';
834
+ const messageParams = `act=${type}${whichParam}`;
821
835
  tellServer(report, messageParams, message);
822
836
  }
823
837
  // Otherwise, i.e. if granular reporting has not been specified:
@@ -829,7 +843,7 @@ const doActs = async (report, actIndex, page) => {
829
843
  actCount++;
830
844
  act.startTime = Date.now();
831
845
  // If the act is an index changer:
832
- if (act.type === 'next') {
846
+ if (type === 'next') {
833
847
  const condition = act.if;
834
848
  const logSuffix = condition.length === 3 ? ` ${condition[1]} ${condition[2]}` : '';
835
849
  console.log(`>> ${condition[0]}${logSuffix}`);
@@ -865,16 +879,16 @@ const doActs = async (report, actIndex, page) => {
865
879
  }
866
880
  }
867
881
  // Otherwise, if the act is a launch:
868
- else if (act.type === 'launch') {
869
- // Launch the specified browser and navigate to the specified URL.
882
+ else if (type === 'launch') {
883
+ // Launch the specified browser on the specified device and navigate to the specified URL.
870
884
  const launchResult = await launch(
871
885
  report,
872
- act.which,
873
- act.url,
886
+ act.url || report.sources.target.url,
874
887
  debug,
875
888
  waits,
876
- act.deviceID || 'default',
877
- act.lowMotion ? 'reduce' : 'no-preference'
889
+ act.deviceID || report.deviceID,
890
+ act.browserID || report.browserID,
891
+ act.lowMotion || report.lowMotion
878
892
  );
879
893
  // If the launch and navigation succeeded:
880
894
  if (launchResult && launchResult.success) {