testaro 21.0.0 → 23.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/run.js CHANGED
@@ -9,10 +9,10 @@
9
9
  require('dotenv').config();
10
10
  // Requirements for acts.
11
11
  const {actSpecs} = require('./actSpecs');
12
+ // Navigation.
13
+ const {browserClose, goTo, launch} = require('./procs/nav');
12
14
  // Module to standardize report formats.
13
- const {standardize} = require('./standardize');
14
- // Playwright package.
15
- const playwright = require('playwright');
15
+ const {standardize} = require('./procs/standardize');
16
16
 
17
17
  // ########## CONSTANTS
18
18
 
@@ -33,7 +33,7 @@ const moves = {
33
33
  select: 'select',
34
34
  text: 'input'
35
35
  };
36
- // Names and descriptions of tests.
36
+ // Names and descriptions of tools.
37
37
  const tests = {
38
38
  alfa: 'alfa',
39
39
  axe: 'Axe',
@@ -45,44 +45,15 @@ const tests = {
45
45
  testaro: 'Testaro',
46
46
  wave: 'WAVE',
47
47
  };
48
- // Browser types available in PlayWright.
49
- const browserTypeNames = {
50
- 'chromium': 'Chrome',
51
- 'webkit': 'Safari',
52
- 'firefox': 'Firefox'
53
- };
54
48
  // Items that may be waited for.
55
49
  const waitables = ['url', 'title', 'body'];
56
- // Strings in log messages indicating errors.
57
- const errorWords = [
58
- 'but not used',
59
- 'content security policy',
60
- 'deprecated',
61
- 'error',
62
- 'exception',
63
- 'expected',
64
- 'failed',
65
- 'invalid',
66
- 'missing',
67
- 'non-standard',
68
- 'not supported',
69
- 'refused',
70
- 'requires',
71
- 'sorry',
72
- 'suspicious',
73
- 'unrecognized',
74
- 'violates',
75
- 'warning'
76
- ];
77
50
 
78
51
  // ########## VARIABLES
79
52
 
80
53
  // Facts about the current session.
81
54
  let actCount = 0;
82
55
  // Facts about the current browser.
83
- let browser;
84
56
  let browserContext;
85
- let browserTypeName;
86
57
  let currentPage;
87
58
  let requestedURL = '';
88
59
 
@@ -238,14 +209,11 @@ const isValidReport = report => {
238
209
  if (acts[0].type !== 'launch') {
239
210
  return 'First act type not launch';
240
211
  }
241
- if (acts[1].type !== 'url') {
242
- return 'Second act type not url';
243
- }
244
212
  if (! ['chromium', 'webkit', 'firefox'].includes(acts[0].which)) {
245
213
  return 'Bad first act which';
246
214
  }
247
- if (! acts[1].which || typeof acts[1].which !== 'string' || ! isURL(acts[1].which)) {
248
- return 'Second act which not a URL';
215
+ if (! acts[0].url || typeof acts[0].url !== 'string' || ! isURL(acts[0].url)) {
216
+ return 'First act url not a URL';
249
217
  }
250
218
  const invalidAct = acts.find(act => ! isValidAct(act));
251
219
  if (invalidAct) {
@@ -278,108 +246,8 @@ const isValidReport = report => {
278
246
 
279
247
  // Returns a string representing the date and time.
280
248
  const nowString = () => (new Date()).toISOString().slice(0, 19);
281
- // Closes the current browser.
282
- const browserClose = async () => {
283
- if (browser) {
284
- const browserType = browser.browserType().name();
285
- let contexts = browser.contexts();
286
- for (const context of contexts) {
287
- await context.close();
288
- contexts = browser.contexts();
289
- }
290
- await browser.close();
291
- browser = null;
292
- console.log(`${browserType} browser closed`);
293
- }
294
- };
295
249
  // Returns the first line of an error message.
296
250
  const errorStart = error => error.message.replace(/\n.+/s, '');
297
- // Launches a browser.
298
- const launch = async (report, typeName, lowMotion = false) => {
299
- const browserType = playwright[typeName];
300
- // If the specified browser type exists:
301
- if (browserType) {
302
- // Close the current browser, if any.
303
- await browserClose();
304
- // Launch a browser of that type.
305
- const browserOptions = {
306
- logger: {
307
- isEnabled: () => false,
308
- log: (name, severity, message) => console.log(message.slice(0, 100))
309
- }
310
- };
311
- if (debug) {
312
- browserOptions.headless = false;
313
- }
314
- if (waits) {
315
- browserOptions.slowMo = waits;
316
- }
317
- let healthy = true;
318
- browser = await browserType.launch(browserOptions)
319
- .catch(error => {
320
- healthy = false;
321
- console.log(`ERROR launching browser (${errorStart(error)})`);
322
- });
323
- // If the launch succeeded:
324
- if (healthy) {
325
- // Open a context (i.e. browser tab), with reduced motion if specified.
326
- const options = {reduceMotion: lowMotion ? 'reduce' : 'no-preference'};
327
- browserContext = await browser.newContext(options);
328
- // When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
329
- browserContext.on('page', async page => {
330
- // Make the page current.
331
- currentPage = page;
332
- // If it emits a message:
333
- page.on('console', msg => {
334
- const msgText = msg.text();
335
- let indentedMsg = '';
336
- // If debugging is on:
337
- if (debug) {
338
- // Log a summary of the message on the console.
339
- const parts = [msgText.slice(0, 75)];
340
- if (msgText.length > 75) {
341
- parts.push(msgText.slice(75, 150));
342
- if (msgText.length > 150) {
343
- const tail = msgText.slice(150).slice(-150);
344
- if (msgText.length > 300) {
345
- parts.push('...');
346
- }
347
- parts.push(tail.slice(0, 75));
348
- if (tail.length > 75) {
349
- parts.push(tail.slice(75));
350
- }
351
- }
352
- }
353
- indentedMsg = parts.map(part => ` | ${part}`).join('\n');
354
- console.log(`\n${indentedMsg}`);
355
- }
356
- // Add statistics on the message to the report.
357
- const msgTextLC = msgText.toLowerCase();
358
- const msgLength = msgText.length;
359
- report.jobData.logCount++;
360
- report.jobData.logSize += msgLength;
361
- if (errorWords.some(word => msgTextLC.includes(word))) {
362
- report.jobData.errorLogCount++;
363
- report.jobData.errorLogSize += msgLength;
364
- }
365
- const msgLC = msgText.toLowerCase();
366
- if (
367
- msgText.includes('403') && (msgLC.includes('status')
368
- || msgLC.includes('prohibited'))
369
- ) {
370
- report.jobData.prohibitedCount++;
371
- }
372
- });
373
- });
374
- // Open the first page of the context.
375
- currentPage = await browserContext.newPage();
376
- // Wait until it is stable.
377
- await currentPage.waitForLoadState('domcontentloaded', {timeout: 15000});
378
- // Update the name of the current browser type and store it in the page.
379
- currentPage.browserTypeName = browserTypeName = typeName;
380
- }
381
- }
382
- };
383
251
  // Normalizes spacing characters and cases in a string.
384
252
  const debloat = string => string.replace(/\s/g, ' ').trim().replace(/ {2,}/g, ' ').toLowerCase();
385
253
  // Returns the text of an element, lower-cased.
@@ -473,8 +341,6 @@ const textOf = async (page, element) => {
473
341
  return null;
474
342
  }
475
343
  };
476
- // Returns a string with any final slash removed.
477
- const deSlash = string => string.endsWith('/') ? string.slice(0, -1) : string;
478
344
  // Returns a property value and whether it satisfies an expectation.
479
345
  const isTrue = (object, specs) => {
480
346
  const property = specs[0];
@@ -557,56 +423,6 @@ const addError = (act, error, message) => {
557
423
  act.result.prevented = true;
558
424
  }
559
425
  };
560
- // Visits a URL and returns the response of the server.
561
- const goTo = async (report, page, url, timeout, waitUntil, isStrict) => {
562
- if (url.startsWith('file://')) {
563
- url = url.replace('file://', `file://${__dirname}/`);
564
- }
565
- // Visit the URL.
566
- const startTime = Date.now();
567
- try {
568
- const response = await page.goto(url, {
569
- timeout,
570
- waitUntil
571
- });
572
- report.jobData.visitLatency += Math.round((Date.now() - startTime) / 1000);
573
- const httpStatus = response.status();
574
- // If the response status was normal:
575
- if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
576
- // If the browser was redirected in violation of a strictness requirement:
577
- const actualURL = page.url();
578
- if (isStrict && deSlash(actualURL) !== deSlash(url)) {
579
- // Return an error.
580
- console.log(`ERROR: Visit to ${url} redirected to ${actualURL}`);
581
- return {
582
- exception: 'badRedirection'
583
- };
584
- }
585
- // Otherwise, i.e. if no prohibited redirection occurred:
586
- else {
587
- // Press the Escape key to dismiss any modal dialog.
588
- await page.keyboard.press('Escape');
589
- // Return the response.
590
- return response;
591
- }
592
- }
593
- // Otherwise, i.e. if the response status was abnormal:
594
- else {
595
- // Return an error.
596
- console.log(`ERROR: Visit to ${url} got status ${httpStatus}`);
597
- report.jobData.visitRejectionCount++;
598
- return {
599
- error: 'badStatus'
600
- };
601
- }
602
- }
603
- catch(error) {
604
- console.log(`ERROR visiting ${url} (${error.message.slice(0, 200)})`);
605
- return {
606
- error: 'noVisit'
607
- };
608
- }
609
- };
610
426
  // Recursively performs the acts in a report.
611
427
  const doActs = async (report, actIndex, page) => {
612
428
  // Quits and reports the performance being aborted.
@@ -678,53 +494,41 @@ const doActs = async (report, actIndex, page) => {
678
494
  }
679
495
  // Otherwise, if the act is a launch:
680
496
  else if (act.type === 'launch') {
681
- // Launch the specified browser, creating a browser context and a page in it.
682
- await launch(report, act.which, act.lowMotion ? 'reduce' : 'no-preference');
683
- // Identify its only page as current.
684
- page = browserContext.pages()[0];
497
+ // Launch the specified browser and navigate to the specified URL.
498
+ const browserData = await launch(
499
+ report, act.which, act.url, debug, waits, act.lowMotion ? 'reduce' : 'no-preference'
500
+ );
501
+ // If the launch and navigation succeeded:
502
+ if (browserData) {
503
+ // Save the browser data.
504
+ browserContext = browserData.browserContext;
505
+ currentPage = browserData.currentPage;
506
+ page = currentPage;
507
+ // Add the actual URL to the act.
508
+ act.actualURL = page.url();
509
+ }
510
+ // Otherwise, i.e. if the launch or navigation failed:
511
+ else {
512
+ // Abort the job.
513
+ await abortActs();
514
+ }
685
515
  }
686
516
  // Otherwise, if a current page exists:
687
517
  else if (page) {
688
- // If the act is a url:
518
+ // If the act is navigation to a url:
689
519
  if (act.type === 'url') {
690
520
  // Identify the URL.
691
521
  const resolved = act.which.replace('__dirname', __dirname);
692
522
  requestedURL = resolved;
693
- const {strict} = report;
694
523
  // Visit it and wait until the DOM is loaded.
695
- let response = await goTo(report, page, requestedURL, 15000, 'domcontentloaded', strict);
696
- // If the visit fails:
697
- if (response.error) {
698
- // Launch another browser type.
699
- const newBrowserName = Object.keys(browserTypeNames)
700
- .find(name => name !== browserTypeName);
701
- console.log(`>> Launching ${newBrowserName} instead`);
702
- await launch(newBrowserName);
703
- // Identify its only page as current.
704
- page = browserContext.pages()[0];
705
- // Visit the URL and wait until the DOM is loaded.
706
- response = await goTo(report, page, requestedURL, 10000, 'domcontentloaded', strict);
707
- // If the visit fails:
708
- if (response.error) {
709
- // Try again and wait until a load.
710
- response = await goTo(report, page, requestedURL, 5000, 'load', strict);
711
- // If the visit fails:
712
- if (response.error) {
713
- // Navigate to a blank page instead.
714
- await page.goto('about:blank')
715
- .catch(error => {
716
- console.log(`ERROR: Navigation to blank page failed (${error.message})`);
717
- });
718
- }
719
- }
720
- }
721
- // If none of the visits succeeded:
524
+ const response = await goTo(report, page, requestedURL, 15000, 'domcontentloaded');
525
+ // If the visit failed:
722
526
  if (response.error) {
723
527
  // Report this and quit.
724
528
  addError(act, 'failure', 'ERROR: Visits failed');
725
529
  await abortActs();
726
530
  }
727
- // Otherwise, i.e. if the last visit attempt succeeded:
531
+ // Otherwise, i.e. if the visit succeeded:
728
532
  else {
729
533
  // If a prohibited redirection occurred:
730
534
  if (response.exception === 'badRedirection') {
@@ -845,535 +649,531 @@ const doActs = async (report, actIndex, page) => {
845
649
  // Otherwise, if the page has a URL:
846
650
  else if (page.url() && page.url() !== 'about:blank') {
847
651
  const url = page.url();
848
- // If redirection is inapplicable, is permitted, or did not occur:
849
- if (
850
- url.startsWith('file:') || ! report.strict || deSlash(url) === deSlash(requestedURL)
851
- ) {
852
- // Add the URL to the act.
853
- act.url = url;
854
- // If the act is a revelation:
855
- if (act.type === 'reveal') {
856
- // Make all elements in the page visible.
857
- await require('./procs/allVis').allVis(page);
858
- act.result = {
859
- success: true
860
- };
652
+ // Add the URL to the act.
653
+ act.actualURL = url;
654
+ // If the act is a revelation:
655
+ if (act.type === 'reveal') {
656
+ // Make all elements in the page visible.
657
+ await require('./procs/allVis').allVis(page);
658
+ act.result = {
659
+ success: true
660
+ };
661
+ }
662
+ // Otherwise, if the act performs tests of a tool:
663
+ else if (act.type === 'test') {
664
+ // Add a description of the test to the act.
665
+ act.what = tests[act.which];
666
+ // Initialize the options argument.
667
+ const options = {};
668
+ // Add any specified arguments to it.
669
+ Object.keys(act).forEach(key => {
670
+ if (! ['type', 'which'].includes(key)) {
671
+ options[key] = act[key];
672
+ }
673
+ });
674
+ // Initialize the test report.
675
+ const startTime = Date.now();
676
+ let toolReport = {
677
+ result: {
678
+ success: false
679
+ }
680
+ };
681
+ // Perform the specified tests of the tool and get a report.
682
+ try {
683
+ const args = [page, options];
684
+ toolReport = await require(`./tests/${act.which}`).reporter(... args);
685
+ toolReport.result.success = true;
861
686
  }
862
- // Otherwise, if the act performs the tests of a tool:
863
- else if (act.type === 'test') {
864
- // Add a description of the test to the act.
865
- act.what = tests[act.which];
866
- // Initialize the options argument.
867
- const options = {};
868
- // Add any specified arguments to it.
869
- Object.keys(act).forEach(key => {
870
- if (! ['type', 'which'].includes(key)) {
871
- options[key] = act[key];
872
- }
873
- });
874
- // Conduct, report, and time the test.
875
- const startTime = Date.now();
876
- let testReport = {
877
- result: {
878
- success: false
879
- }
687
+ // If the testing failed:
688
+ catch(error) {
689
+ // Report this but do not abort the job.
690
+ console.log(`ERROR: Test act ${act.which} failed (${error.message.slice(0, 400)})`);
691
+ }
692
+ // Add the elapsed time to the report.
693
+ const time = Math.round((Date.now() - startTime) / 1000);
694
+ const {toolTimes} = report.jobData;
695
+ if (! toolTimes[act.which]) {
696
+ toolTimes[act.which] = 0;
697
+ }
698
+ toolTimes[act.which] += time;
699
+ // Add the result object (possibly an array) to the act.
700
+ const resultCount = Object.keys(toolReport.result).length;
701
+ act.result = resultCount ? toolReport.result : {success: false};
702
+ // If a standard-format result is to be included in the report:
703
+ const standard = process.env.STANDARD;
704
+ if (['also', 'only'].includes(standard)) {
705
+ // Initialize it.
706
+ act.standardResult = {
707
+ totals: [],
708
+ instances: []
880
709
  };
881
- try {
882
- const args = [page, options];
883
- testReport = await require(`./tests/${act.which}`).reporter(... args);
884
- testReport.result.success = true;
710
+ // Populate it.
711
+ standardize(act);
712
+ // If the original-format result is not to be included in the report:
713
+ if (standard === 'only') {
714
+ // Remove it.
715
+ delete act.result;
885
716
  }
886
- catch(error) {
887
- console.log(`ERROR: Test act ${act.which} failed (${error.message.slice(0, 400)})`);
888
- }
889
- report.jobData.testTimes.push(
890
- [act.which, Math.round((Date.now() - startTime) / 1000)]
891
- );
892
- report.jobData.testTimes.sort((a, b) => b[1] - a[1]);
893
- // Add the result object (possibly an array) to the act.
894
- const resultCount = Object.keys(testReport.result).length;
895
- act.result = resultCount ? testReport.result : {success: false};
896
- // If a standard-format result is to be included in the report:
897
- const standard = process.env.STANDARD;
898
- if (['also', 'only'].includes(standard)) {
899
- // Initialize it.
900
- act.standardResult = {
901
- totals: [],
902
- instances: []
903
- };
904
- // Populate it.
905
- standardize(act);
906
- // If the original-format result is not to be included in the report:
907
- if (standard === 'only') {
908
- // Remove it.
909
- delete act.result;
910
- }
911
- const expectations = act.expect;
912
- // If the test has expectations:
913
- if (expectations) {
914
- // Initialize whether they were fulfilled.
915
- act.expectations = [];
916
- let failureCount = 0;
917
- // For each expectation:
918
- expectations.forEach(spec => {
919
- const truth = isTrue(act, spec);
920
- act.expectations.push({
921
- property: spec[0],
922
- relation: spec[1],
923
- criterion: spec[2],
924
- actual: truth[0],
925
- passed: truth[1]
926
- });
927
- if (! truth[1]) {
928
- failureCount++;
929
- }
717
+ // If the test has expectations:
718
+ const expectations = act.expect;
719
+ if (expectations) {
720
+ // Initialize whether they were fulfilled.
721
+ act.expectations = [];
722
+ let failureCount = 0;
723
+ // For each expectation:
724
+ expectations.forEach(spec => {
725
+ const truth = isTrue(act, spec);
726
+ act.expectations.push({
727
+ property: spec[0],
728
+ relation: spec[1],
729
+ criterion: spec[2],
730
+ actual: truth[0],
731
+ passed: truth[1]
930
732
  });
931
- act.expectationFailures = failureCount;
932
- }
733
+ if (! truth[1]) {
734
+ failureCount++;
735
+ }
736
+ });
737
+ act.expectationFailures = failureCount;
933
738
  }
934
739
  }
935
- // Otherwise, if the act is a move:
936
- else if (moves[act.type]) {
937
- const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
938
- // Try up to 5 times to:
939
- act.result = {found: false};
940
- let selection = {};
941
- let tries = 0;
942
- const slimText = act.which ? debloat(act.which) : '';
943
- while (tries++ < 5 && ! act.result.found) {
944
- if (page) {
945
- // Identify the elements of the specified type.
946
- const selections = await page.$$(selector);
947
- // If there are any:
948
- if (selections.length) {
949
- // If there are enough to make a match possible:
950
- if ((act.index || 0) < selections.length) {
951
- // For each element of the specified type:
952
- let matchCount = 0;
953
- const selectionTexts = [];
954
- for (selection of selections) {
955
- // Add its lower-case text or an empty string to the list of element texts.
956
- const selectionText = slimText ? await textOf(page, selection) : '';
957
- selectionTexts.push(selectionText);
958
- // If its text includes any specified text, case-insensitively:
959
- if (selectionText.includes(slimText)) {
960
- // If the element has the specified index among such elements:
961
- if (matchCount++ === (act.index || 0)) {
962
- // Report it as the matching element and stop checking.
963
- act.result.found = true;
964
- act.result.textSpec = slimText;
965
- act.result.textContent = selectionText;
966
- break;
967
- }
968
- }
969
- }
970
- // If no element satisfied the specifications:
971
- if (! act.result.found) {
972
- // Add the failure data to the report.
973
- act.result.success = false;
974
- act.result.error = 'exhausted';
975
- act.result.typeElementCount = selections.length;
976
- if (slimText) {
977
- act.result.textElementCount = --matchCount;
740
+ }
741
+ // Otherwise, if the act is a move:
742
+ else if (moves[act.type]) {
743
+ const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
744
+ // Try up to 5 times to:
745
+ act.result = {found: false};
746
+ let selection = {};
747
+ let tries = 0;
748
+ const slimText = act.which ? debloat(act.which) : '';
749
+ while (tries++ < 5 && ! act.result.found) {
750
+ if (page) {
751
+ // Identify the elements of the specified type.
752
+ const selections = await page.$$(selector);
753
+ // If there are any:
754
+ if (selections.length) {
755
+ // If there are enough to make a match possible:
756
+ if ((act.index || 0) < selections.length) {
757
+ // For each element of the specified type:
758
+ let matchCount = 0;
759
+ const selectionTexts = [];
760
+ for (selection of selections) {
761
+ // Add its lower-case text or an empty string to the list of element texts.
762
+ const selectionText = slimText ? await textOf(page, selection) : '';
763
+ selectionTexts.push(selectionText);
764
+ // If its text includes any specified text, case-insensitively:
765
+ if (selectionText.includes(slimText)) {
766
+ // If the element has the specified index among such elements:
767
+ if (matchCount++ === (act.index || 0)) {
768
+ // Report it as the matching element and stop checking.
769
+ act.result.found = true;
770
+ act.result.textSpec = slimText;
771
+ act.result.textContent = selectionText;
772
+ break;
978
773
  }
979
- act.result.message = 'Not enough specified elements exist';
980
- act.result.candidateTexts = selectionTexts;
981
774
  }
982
775
  }
983
- // Otherwise, i.e. if there are too few such elements to make a match possible:
984
- else {
776
+ // If no element satisfied the specifications:
777
+ if (! act.result.found) {
985
778
  // Add the failure data to the report.
986
779
  act.result.success = false;
987
- act.result.error = 'fewer';
780
+ act.result.error = 'exhausted';
988
781
  act.result.typeElementCount = selections.length;
989
- act.result.message = 'Elements of specified type too few';
782
+ if (slimText) {
783
+ act.result.textElementCount = --matchCount;
784
+ }
785
+ act.result.message = 'Not enough specified elements exist';
786
+ act.result.candidateTexts = selectionTexts;
990
787
  }
991
788
  }
992
- // Otherwise, i.e. if there are no elements of the specified type:
789
+ // Otherwise, i.e. if there are too few such elements to make a match possible:
993
790
  else {
994
791
  // Add the failure data to the report.
995
792
  act.result.success = false;
996
- act.result.error = 'none';
997
- act.result.typeElementCount = 0;
998
- act.result.message = 'No elements of specified type found';
793
+ act.result.error = 'fewer';
794
+ act.result.typeElementCount = selections.length;
795
+ act.result.message = 'Elements of specified type too few';
999
796
  }
1000
797
  }
1001
- // Otherwise, i.e. if the page no longer exists:
798
+ // Otherwise, i.e. if there are no elements of the specified type:
1002
799
  else {
1003
800
  // Add the failure data to the report.
1004
801
  act.result.success = false;
1005
- act.result.error = 'gone';
1006
- act.result.message = 'Page gone';
1007
- }
1008
- if (! act.result.found) {
1009
- await wait(2000);
802
+ act.result.error = 'none';
803
+ act.result.typeElementCount = 0;
804
+ act.result.message = 'No elements of specified type found';
1010
805
  }
1011
806
  }
1012
- // If a match was found:
1013
- if (act.result.found) {
1014
- // FUNCTION DEFINITION START
1015
- // Performs a click or Enter keypress and waits for the network to be idle.
1016
- const doAndWait = async isClick => {
1017
- // Perform and report the move.
1018
- const move = isClick ? 'click' : 'Enter keypress';
807
+ // Otherwise, i.e. if the page no longer exists:
808
+ else {
809
+ // Add the failure data to the report.
810
+ act.result.success = false;
811
+ act.result.error = 'gone';
812
+ act.result.message = 'Page gone';
813
+ }
814
+ if (! act.result.found) {
815
+ await wait(2000);
816
+ }
817
+ }
818
+ // If a match was found:
819
+ if (act.result.found) {
820
+ // FUNCTION DEFINITION START
821
+ // Performs a click or Enter keypress and waits for the network to be idle.
822
+ const doAndWait = async isClick => {
823
+ // Perform and report the move.
824
+ const move = isClick ? 'click' : 'Enter keypress';
825
+ try {
826
+ await isClick
827
+ ? selection.click({timeout: 4000})
828
+ : selection.press('Enter', {timeout: 4000});
829
+ act.result.success = true;
830
+ act.result.move = move;
831
+ }
832
+ // If the move fails:
833
+ catch(error) {
834
+ // Quit and add failure data to the report.
835
+ act.result.success = false;
836
+ act.result.error = 'moveFailure';
837
+ act.result.message = `ERROR: ${move} failed`;
838
+ console.log(`ERROR: ${move} failed (${errorStart(error)})`);
839
+ await abortActs();
840
+ }
841
+ if (act.result.success) {
1019
842
  try {
1020
- await isClick
1021
- ? selection.click({timeout: 4000})
1022
- : selection.press('Enter', {timeout: 4000});
1023
- act.result.success = true;
1024
- act.result.move = move;
843
+ await page.context().waitForEvent('networkidle', {timeout: 10000});
844
+ act.result.idleTimely = true;
1025
845
  }
1026
- // If the move fails:
1027
846
  catch(error) {
1028
- // Quit and add failure data to the report.
1029
- act.result.success = false;
1030
- act.result.error = 'moveFailure';
1031
- act.result.message = `ERROR: ${move} failed`;
1032
- console.log(`ERROR: ${move} failed (${errorStart(error)})`);
1033
- await abortActs();
1034
- }
1035
- if (act.result.success) {
1036
- try {
1037
- await page.context().waitForEvent('networkidle', {timeout: 10000});
1038
- act.result.idleTimely = true;
1039
- }
1040
- catch(error) {
1041
- console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
1042
- act.result.idleTimely = false;
1043
- }
1044
- // If the move created a new page, make it current.
1045
- page = currentPage;
1046
- act.result.newURL = page.url();
847
+ console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
848
+ act.result.idleTimely = false;
1047
849
  }
1048
- };
1049
- // FUNCTION DEFINITION END
1050
- // If the move is a button click, perform it.
1051
- if (act.type === 'button') {
1052
- await selection.click({timeout: 3000});
1053
- act.result.success = true;
1054
- act.result.move = 'clicked';
850
+ // If the move created a new page, make it current.
851
+ page = currentPage;
852
+ act.result.newURL = page.url();
1055
853
  }
1056
- // Otherwise, if it is checking a radio button or checkbox, perform it.
1057
- else if (['checkbox', 'radio'].includes(act.type)) {
1058
- await selection.waitForElementState('stable', {timeout: 2000})
1059
- .catch(error => {
1060
- console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
1061
- act.result.success = false;
1062
- act.result.error = `ERROR waiting for stable ${act.type}`;
1063
- });
1064
- if (! act.result.error) {
1065
- const isEnabled = await selection.isEnabled();
1066
- if (isEnabled) {
1067
- await selection.check({
1068
- force: true,
1069
- timeout: 2000
1070
- })
1071
- .catch(error => {
1072
- console.log(`ERROR checking ${act.type} (${error.message})`);
1073
- act.result.success = false;
1074
- act.result.error = `ERROR checking ${act.type}`;
1075
- });
1076
- if (! act.result.error) {
1077
- act.result.success = true;
1078
- act.result.move = 'checked';
1079
- }
1080
- }
1081
- else {
1082
- const report = `ERROR: could not check ${act.type} because disabled`;
1083
- console.log(report);
854
+ };
855
+ // FUNCTION DEFINITION END
856
+ // If the move is a button click, perform it.
857
+ if (act.type === 'button') {
858
+ await selection.click({timeout: 3000});
859
+ act.result.success = true;
860
+ act.result.move = 'clicked';
861
+ }
862
+ // Otherwise, if it is checking a radio button or checkbox, perform it.
863
+ else if (['checkbox', 'radio'].includes(act.type)) {
864
+ await selection.waitForElementState('stable', {timeout: 2000})
865
+ .catch(error => {
866
+ console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
867
+ act.result.success = false;
868
+ act.result.error = `ERROR waiting for stable ${act.type}`;
869
+ });
870
+ if (! act.result.error) {
871
+ const isEnabled = await selection.isEnabled();
872
+ if (isEnabled) {
873
+ await selection.check({
874
+ force: true,
875
+ timeout: 2000
876
+ })
877
+ .catch(error => {
878
+ console.log(`ERROR checking ${act.type} (${error.message})`);
1084
879
  act.result.success = false;
1085
- act.result.error = report;
880
+ act.result.error = `ERROR checking ${act.type}`;
881
+ });
882
+ if (! act.result.error) {
883
+ act.result.success = true;
884
+ act.result.move = 'checked';
1086
885
  }
1087
886
  }
887
+ else {
888
+ const report = `ERROR: could not check ${act.type} because disabled`;
889
+ console.log(report);
890
+ act.result.success = false;
891
+ act.result.error = report;
892
+ }
1088
893
  }
1089
- // Otherwise, if it is focusing the element, perform it.
1090
- else if (act.type === 'focus') {
1091
- await selection.focus({timeout: 2000});
1092
- act.result.success = true;
1093
- act.result.move = 'focused';
894
+ }
895
+ // Otherwise, if it is focusing the element, perform it.
896
+ else if (act.type === 'focus') {
897
+ await selection.focus({timeout: 2000});
898
+ act.result.success = true;
899
+ act.result.move = 'focused';
900
+ }
901
+ // Otherwise, if it is clicking a link:
902
+ else if (act.type === 'link') {
903
+ const href = await selection.getAttribute('href');
904
+ const target = await selection.getAttribute('target');
905
+ act.result.href = href || 'NONE';
906
+ act.result.target = target || 'DEFAULT';
907
+ // If the destination is a new page:
908
+ if (target && target !== '_self') {
909
+ // Click the link and wait for the network to be idle.
910
+ doAndWait(true);
1094
911
  }
1095
- // Otherwise, if it is clicking a link:
1096
- else if (act.type === 'link') {
1097
- const href = await selection.getAttribute('href');
1098
- const target = await selection.getAttribute('target');
1099
- act.result.href = href || 'NONE';
1100
- act.result.target = target || 'DEFAULT';
1101
- // If the destination is a new page:
1102
- if (target && target !== '_self') {
1103
- // Click the link and wait for the network to be idle.
1104
- doAndWait(true);
912
+ // Otherwise, i.e. if the destination is in the current page:
913
+ else {
914
+ // Click the link and wait for the resulting navigation.
915
+ try {
916
+ await selection.click({timeout: 5000});
917
+ // Wait for the new content to load.
918
+ await page.waitForLoadState('domcontentloaded', {timeout: 6000});
919
+ act.result.success = true;
920
+ act.result.move = 'clicked';
921
+ act.result.newURL = page.url();
1105
922
  }
1106
- // Otherwise, i.e. if the destination is in the current page:
1107
- else {
1108
- // Click the link and wait for the resulting navigation.
1109
- try {
1110
- await selection.click({timeout: 5000});
1111
- // Wait for the new content to load.
1112
- await page.waitForLoadState('domcontentloaded', {timeout: 6000});
1113
- act.result.success = true;
1114
- act.result.move = 'clicked';
1115
- act.result.newURL = page.url();
1116
- }
1117
- // If the click or load failed:
1118
- catch(error) {
1119
- // Quit and add failure data to the report.
1120
- console.log(`ERROR clicking link (${errorStart(error)})`);
1121
- act.result.success = false;
1122
- act.result.error = 'unclickable';
1123
- act.result.message = 'ERROR: click or load timed out';
1124
- await abortActs();
1125
- }
1126
- // If the link click succeeded:
1127
- if (! act.result.error) {
1128
- // Add success data to the report.
1129
- act.result.success = true;
1130
- act.result.move = 'clicked';
1131
- }
923
+ // If the click or load failed:
924
+ catch(error) {
925
+ // Quit and add failure data to the report.
926
+ console.log(`ERROR clicking link (${errorStart(error)})`);
927
+ act.result.success = false;
928
+ act.result.error = 'unclickable';
929
+ act.result.message = 'ERROR: click or load timed out';
930
+ await abortActs();
1132
931
  }
1133
- }
1134
- // Otherwise, if it is selecting an option in a select list, perform it.
1135
- else if (act.type === 'select') {
1136
- const options = await selection.$$('option');
1137
- let optionText = '';
1138
- if (options && Array.isArray(options) && options.length) {
1139
- const optionTexts = [];
1140
- for (const option of options) {
1141
- const optionText = await option.textContent();
1142
- optionTexts.push(optionText);
1143
- }
1144
- const matchTexts = optionTexts.map(
1145
- (text, index) => text.includes(act.what) ? index : -1
1146
- );
1147
- const index = matchTexts.filter(text => text > -1)[act.index || 0];
1148
- if (index !== undefined) {
1149
- await selection.selectOption({index});
1150
- optionText = optionTexts[index];
1151
- }
932
+ // If the link click succeeded:
933
+ if (! act.result.error) {
934
+ // Add success data to the report.
935
+ act.result.success = true;
936
+ act.result.move = 'clicked';
1152
937
  }
1153
- act.result.success = true;
1154
- act.result.move = 'selected';
1155
- act.result.option = optionText;
1156
938
  }
1157
- // Otherwise, if it is entering text in an input element:
1158
- else if (['text', 'search'].includes(act.type)) {
1159
- act.result.attributes = {};
1160
- const {attributes} = act.result;
1161
- const type = await selection.getAttribute('type');
1162
- const label = await selection.getAttribute('aria-label');
1163
- const labelRefs = await selection.getAttribute('aria-labelledby');
1164
- attributes.type = type || '';
1165
- attributes.label = label || '';
1166
- attributes.labelRefs = labelRefs || '';
1167
- // If the text contains a placeholder for an environment variable:
1168
- let {what} = act;
1169
- if (/__[A-Z]+__/.test(what)) {
1170
- // Replace it.
1171
- const envKey = /__([A-Z]+)__/.exec(what)[1];
1172
- const envValue = process.env[envKey];
1173
- what = what.replace(/__[A-Z]+__/, envValue);
939
+ }
940
+ // Otherwise, if it is selecting an option in a select list, perform it.
941
+ else if (act.type === 'select') {
942
+ const options = await selection.$$('option');
943
+ let optionText = '';
944
+ if (options && Array.isArray(options) && options.length) {
945
+ const optionTexts = [];
946
+ for (const option of options) {
947
+ const optionText = await option.textContent();
948
+ optionTexts.push(optionText);
1174
949
  }
1175
- // Enter the text.
1176
- await selection.type(act.what);
1177
- report.jobData.presses += act.what.length;
1178
- act.result.success = true;
1179
- act.result.move = 'entered';
1180
- // If the input is a search input:
1181
- if (act.type === 'search') {
1182
- // Press the Enter key and wait for a network to be idle.
1183
- doAndWait(false);
950
+ const matchTexts = optionTexts.map(
951
+ (text, index) => text.includes(act.what) ? index : -1
952
+ );
953
+ const index = matchTexts.filter(text => text > -1)[act.index || 0];
954
+ if (index !== undefined) {
955
+ await selection.selectOption({index});
956
+ optionText = optionTexts[index];
1184
957
  }
1185
958
  }
1186
- // Otherwise, i.e. if the move is unknown, add the failure to the act.
1187
- else {
1188
- // Report the error.
1189
- const report = 'ERROR: move unknown';
1190
- act.result.success = false;
1191
- act.result.error = report;
1192
- console.log(report);
959
+ act.result.success = true;
960
+ act.result.move = 'selected';
961
+ act.result.option = optionText;
962
+ }
963
+ // Otherwise, if it is entering text in an input element:
964
+ else if (['text', 'search'].includes(act.type)) {
965
+ act.result.attributes = {};
966
+ const {attributes} = act.result;
967
+ const type = await selection.getAttribute('type');
968
+ const label = await selection.getAttribute('aria-label');
969
+ const labelRefs = await selection.getAttribute('aria-labelledby');
970
+ attributes.type = type || '';
971
+ attributes.label = label || '';
972
+ attributes.labelRefs = labelRefs || '';
973
+ // If the text contains a placeholder for an environment variable:
974
+ let {what} = act;
975
+ if (/__[A-Z]+__/.test(what)) {
976
+ // Replace it.
977
+ const envKey = /__([A-Z]+)__/.exec(what)[1];
978
+ const envValue = process.env[envKey];
979
+ what = what.replace(/__[A-Z]+__/, envValue);
980
+ }
981
+ // Enter the text.
982
+ await selection.type(act.what);
983
+ report.jobData.presses += act.what.length;
984
+ act.result.success = true;
985
+ act.result.move = 'entered';
986
+ // If the input is a search input:
987
+ if (act.type === 'search') {
988
+ // Press the Enter key and wait for a network to be idle.
989
+ doAndWait(false);
1193
990
  }
1194
991
  }
1195
- // Otherwise, i.e. if no match was found:
992
+ // Otherwise, i.e. if the move is unknown, add the failure to the act.
1196
993
  else {
1197
- // Quit and add failure data to the report.
994
+ // Report the error.
995
+ const report = 'ERROR: move unknown';
1198
996
  act.result.success = false;
1199
- act.result.error = 'absent';
1200
- act.result.message = 'ERROR: specified element not found';
1201
- console.log('ERROR: Specified element not found');
1202
- await abortActs();
997
+ act.result.error = report;
998
+ console.log(report);
1203
999
  }
1204
1000
  }
1205
- // Otherwise, if the act is a keypress:
1206
- else if (act.type === 'press') {
1207
- // Identify the number of times to press the key.
1208
- let times = 1 + (act.again || 0);
1209
- report.jobData.presses += times;
1210
- const key = act.which;
1211
- // Press the key.
1212
- while (times--) {
1213
- await page.keyboard.press(key);
1214
- }
1215
- const qualifier = act.again ? `${1 + act.again} times` : 'once';
1216
- act.result = {
1217
- success: true,
1218
- message: `pressed ${qualifier}`
1219
- };
1001
+ // Otherwise, i.e. if no match was found:
1002
+ else {
1003
+ // Quit and add failure data to the report.
1004
+ act.result.success = false;
1005
+ act.result.error = 'absent';
1006
+ act.result.message = 'ERROR: specified element not found';
1007
+ console.log('ERROR: Specified element not found');
1008
+ await abortActs();
1220
1009
  }
1221
- // Otherwise, if it is a repetitive keyboard navigation:
1222
- else if (act.type === 'presses') {
1223
- const {navKey, what, which, withItems} = act;
1224
- const matchTexts = which ? which.map(text => debloat(text)) : [];
1225
- // Initialize the loop variables.
1226
- let status = 'more';
1227
- let presses = 0;
1228
- let amountRead = 0;
1229
- let items = [];
1230
- let matchedText;
1231
- // As long as a matching element has not been reached:
1232
- while (status === 'more') {
1233
- // Press the Escape key to dismiss any modal dialog.
1234
- await page.keyboard.press('Escape');
1235
- // Press the specified navigation key.
1236
- await page.keyboard.press(navKey);
1237
- presses++;
1238
- // Identify the newly current element or a failure.
1239
- const currentJSHandle = await page.evaluateHandle(actCount => {
1240
- // Initialize it as the focused element.
1241
- let currentElement = document.activeElement;
1242
- // If it exists in the page:
1243
- if (currentElement && currentElement.tagName !== 'BODY') {
1244
- // Change it, if necessary, to its active descendant.
1245
- if (currentElement.hasAttribute('aria-activedescendant')) {
1246
- currentElement = document.getElementById(
1247
- currentElement.getAttribute('aria-activedescendant')
1248
- );
1249
- }
1250
- // Or change it, if necessary, to its selected option.
1251
- else if (currentElement.tagName === 'SELECT') {
1252
- const currentIndex = Math.max(0, currentElement.selectedIndex);
1253
- const options = currentElement.querySelectorAll('option');
1254
- currentElement = options[currentIndex];
1255
- }
1256
- // Or change it, if necessary, to its active shadow-DOM element.
1257
- else if (currentElement.shadowRoot) {
1258
- currentElement = currentElement.shadowRoot.activeElement;
1259
- }
1260
- // If there is a current element:
1261
- if (currentElement) {
1262
- // If it was already reached within this act:
1263
- if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1264
- // Report the error.
1265
- console.log(`ERROR: ${currentElement.tagName} element reached again`);
1266
- status = 'ERROR';
1267
- return 'ERROR: locallyExhausted';
1268
- }
1269
- // Otherwise, i.e. if it is newly reached within this act:
1270
- else {
1271
- // Mark and return it.
1272
- currentElement.dataset.pressesReached = actCount;
1273
- return currentElement;
1274
- }
1275
- }
1276
- // Otherwise, i.e. if there is no current element:
1277
- else {
1010
+ }
1011
+ // Otherwise, if the act is a keypress:
1012
+ else if (act.type === 'press') {
1013
+ // Identify the number of times to press the key.
1014
+ let times = 1 + (act.again || 0);
1015
+ report.jobData.presses += times;
1016
+ const key = act.which;
1017
+ // Press the key.
1018
+ while (times--) {
1019
+ await page.keyboard.press(key);
1020
+ }
1021
+ const qualifier = act.again ? `${1 + act.again} times` : 'once';
1022
+ act.result = {
1023
+ success: true,
1024
+ message: `pressed ${qualifier}`
1025
+ };
1026
+ }
1027
+ // Otherwise, if it is a repetitive keyboard navigation:
1028
+ else if (act.type === 'presses') {
1029
+ const {navKey, what, which, withItems} = act;
1030
+ const matchTexts = which ? which.map(text => debloat(text)) : [];
1031
+ // Initialize the loop variables.
1032
+ let status = 'more';
1033
+ let presses = 0;
1034
+ let amountRead = 0;
1035
+ let items = [];
1036
+ let matchedText;
1037
+ // As long as a matching element has not been reached:
1038
+ while (status === 'more') {
1039
+ // Press the Escape key to dismiss any modal dialog.
1040
+ await page.keyboard.press('Escape');
1041
+ // Press the specified navigation key.
1042
+ await page.keyboard.press(navKey);
1043
+ presses++;
1044
+ // Identify the newly current element or a failure.
1045
+ const currentJSHandle = await page.evaluateHandle(actCount => {
1046
+ // Initialize it as the focused element.
1047
+ let currentElement = document.activeElement;
1048
+ // If it exists in the page:
1049
+ if (currentElement && currentElement.tagName !== 'BODY') {
1050
+ // Change it, if necessary, to its active descendant.
1051
+ if (currentElement.hasAttribute('aria-activedescendant')) {
1052
+ currentElement = document.getElementById(
1053
+ currentElement.getAttribute('aria-activedescendant')
1054
+ );
1055
+ }
1056
+ // Or change it, if necessary, to its selected option.
1057
+ else if (currentElement.tagName === 'SELECT') {
1058
+ const currentIndex = Math.max(0, currentElement.selectedIndex);
1059
+ const options = currentElement.querySelectorAll('option');
1060
+ currentElement = options[currentIndex];
1061
+ }
1062
+ // Or change it, if necessary, to its active shadow-DOM element.
1063
+ else if (currentElement.shadowRoot) {
1064
+ currentElement = currentElement.shadowRoot.activeElement;
1065
+ }
1066
+ // If there is a current element:
1067
+ if (currentElement) {
1068
+ // If it was already reached within this act:
1069
+ if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1278
1070
  // Report the error.
1071
+ console.log(`ERROR: ${currentElement.tagName} element reached again`);
1279
1072
  status = 'ERROR';
1280
- return 'noActiveElement';
1073
+ return 'ERROR: locallyExhausted';
1074
+ }
1075
+ // Otherwise, i.e. if it is newly reached within this act:
1076
+ else {
1077
+ // Mark and return it.
1078
+ currentElement.dataset.pressesReached = actCount;
1079
+ return currentElement;
1281
1080
  }
1282
1081
  }
1283
- // Otherwise, i.e. if there is no focus in the page:
1082
+ // Otherwise, i.e. if there is no current element:
1284
1083
  else {
1285
1084
  // Report the error.
1286
1085
  status = 'ERROR';
1287
- return 'ERROR: globallyExhausted';
1086
+ return 'noActiveElement';
1087
+ }
1088
+ }
1089
+ // Otherwise, i.e. if there is no focus in the page:
1090
+ else {
1091
+ // Report the error.
1092
+ status = 'ERROR';
1093
+ return 'ERROR: globallyExhausted';
1094
+ }
1095
+ }, actCount);
1096
+ // If the current element exists:
1097
+ const currentElement = currentJSHandle.asElement();
1098
+ if (currentElement) {
1099
+ // Update the data.
1100
+ const tagNameJSHandle = await currentElement.getProperty('tagName');
1101
+ const tagName = await tagNameJSHandle.jsonValue();
1102
+ const text = await textOf(page, currentElement);
1103
+ // If the text of the current element was found:
1104
+ if (text !== null) {
1105
+ const textLength = text.length;
1106
+ // If it is non-empty and there are texts to match:
1107
+ if (matchTexts.length && textLength) {
1108
+ // Identify the matching text.
1109
+ matchedText = matchTexts.find(matchText => text.includes(matchText));
1288
1110
  }
1289
- }, actCount);
1290
- // If the current element exists:
1291
- const currentElement = currentJSHandle.asElement();
1292
- if (currentElement) {
1293
- // Update the data.
1294
- const tagNameJSHandle = await currentElement.getProperty('tagName');
1295
- const tagName = await tagNameJSHandle.jsonValue();
1296
- const text = await textOf(page, currentElement);
1297
- // If the text of the current element was found:
1298
- if (text !== null) {
1299
- const textLength = text.length;
1300
- // If it is non-empty and there are texts to match:
1301
- if (matchTexts.length && textLength) {
1302
- // Identify the matching text.
1303
- matchedText = matchTexts.find(matchText => text.includes(matchText));
1111
+ // Update the item data if required.
1112
+ if (withItems) {
1113
+ const itemData = {
1114
+ tagName,
1115
+ text,
1116
+ textLength
1117
+ };
1118
+ if (matchedText) {
1119
+ itemData.matchedText = matchedText;
1304
1120
  }
1305
- // Update the item data if required.
1306
- if (withItems) {
1307
- const itemData = {
1308
- tagName,
1309
- text,
1310
- textLength
1311
- };
1312
- if (matchedText) {
1313
- itemData.matchedText = matchedText;
1121
+ items.push(itemData);
1122
+ }
1123
+ amountRead += textLength;
1124
+ // If there is no text-match failure:
1125
+ if (matchedText || ! matchTexts.length) {
1126
+ // If the element has any specified tag name:
1127
+ if (! what || tagName === what) {
1128
+ // Change the status.
1129
+ status = 'done';
1130
+ // Perform the action.
1131
+ const inputText = act.text;
1132
+ if (inputText) {
1133
+ await page.keyboard.type(inputText);
1134
+ presses += inputText.length;
1314
1135
  }
1315
- items.push(itemData);
1316
- }
1317
- amountRead += textLength;
1318
- // If there is no text-match failure:
1319
- if (matchedText || ! matchTexts.length) {
1320
- // If the element has any specified tag name:
1321
- if (! what || tagName === what) {
1322
- // Change the status.
1323
- status = 'done';
1324
- // Perform the action.
1325
- const inputText = act.text;
1326
- if (inputText) {
1327
- await page.keyboard.type(inputText);
1328
- presses += inputText.length;
1329
- }
1330
- if (act.action) {
1331
- presses++;
1332
- await page.keyboard.press(act.action);
1333
- await page.waitForLoadState();
1334
- }
1136
+ if (act.action) {
1137
+ presses++;
1138
+ await page.keyboard.press(act.action);
1139
+ await page.waitForLoadState();
1335
1140
  }
1336
1141
  }
1337
1142
  }
1338
- else {
1339
- status = 'ERROR';
1340
- }
1341
1143
  }
1342
- // Otherwise, i.e. if there was a failure:
1343
1144
  else {
1344
- // Update the status.
1345
- status = await currentJSHandle.jsonValue();
1145
+ status = 'ERROR';
1346
1146
  }
1347
1147
  }
1348
- // Add the result to the act.
1349
- act.result = {
1350
- success: true,
1351
- status,
1352
- totals: {
1353
- presses,
1354
- amountRead
1355
- }
1356
- };
1357
- if (status === 'done' && matchedText) {
1358
- act.result.matchedText = matchedText;
1148
+ // Otherwise, i.e. if there was a failure:
1149
+ else {
1150
+ // Update the status.
1151
+ status = await currentJSHandle.jsonValue();
1359
1152
  }
1360
- if (withItems) {
1361
- act.result.items = items;
1153
+ }
1154
+ // Add the result to the act.
1155
+ act.result = {
1156
+ success: true,
1157
+ status,
1158
+ totals: {
1159
+ presses,
1160
+ amountRead
1362
1161
  }
1363
- // Add the totals to the report.
1364
- report.jobData.presses += presses;
1365
- report.jobData.amountRead += amountRead;
1162
+ };
1163
+ if (status === 'done' && matchedText) {
1164
+ act.result.matchedText = matchedText;
1366
1165
  }
1367
- // Otherwise, i.e. if the act type is unknown:
1368
- else {
1369
- // Add the error result to the act.
1370
- addError(act, 'badType', 'ERROR: Invalid act type');
1166
+ if (withItems) {
1167
+ act.result.items = items;
1371
1168
  }
1169
+ // Add the totals to the report.
1170
+ report.jobData.presses += presses;
1171
+ report.jobData.amountRead += amountRead;
1372
1172
  }
1373
- // Otherwise, i.e. if redirection is prohibited but occurred:
1173
+ // Otherwise, i.e. if the act type is unknown:
1374
1174
  else {
1375
- // Add an error result to the act.
1376
- addError(act, 'redirection', `ERROR: Page redirected to (${url})`);
1175
+ // Add the error result to the act.
1176
+ addError(act, 'badType', 'ERROR: Invalid act type');
1377
1177
  }
1378
1178
  }
1379
1179
  // Otherwise, a page URL is required but does not exist, so:
@@ -1434,12 +1234,22 @@ exports.doJob = async report => {
1434
1234
  report.jobData.abortedAct = null;
1435
1235
  report.jobData.presses = 0;
1436
1236
  report.jobData.amountRead = 0;
1437
- report.jobData.testTimes = [];
1438
- // Recursively perform the acts and get any page if debugging is on and a job was aborted.
1237
+ report.jobData.toolTimes = {};
1238
+ // Recursively perform the acts.
1439
1239
  await doActs(report, 0, null);
1440
1240
  // Add the end time and duration to the report.
1441
1241
  const endTime = new Date();
1442
1242
  report.jobData.endTime = nowString();
1443
1243
  report.jobData.elapsedSeconds = Math.floor((endTime - startTime) / 1000);
1244
+ // Consolidate and sort the tool times.
1245
+ const {toolTimes} = report.jobData;
1246
+ const toolTimeData = Object
1247
+ .keys(toolTimes)
1248
+ .sort((a, b) => toolTimes[b] - toolTimes[a])
1249
+ .map(tool => [tool, toolTimes[tool]]);
1250
+ report.jobData.toolTimes = {};
1251
+ toolTimeData.forEach(item => {
1252
+ report.jobData.toolTimes[item[0]] = item[1];
1253
+ });
1444
1254
  }
1445
1255
  };