testaro 47.2.1 → 48.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
@@ -29,6 +29,8 @@
29
29
 
30
30
  // IMPORTS
31
31
 
32
+ // Module to perform file operations.
33
+ const fs = require('fs/promises');
32
34
  // Module to keep secrets.
33
35
  require('dotenv').config();
34
36
  // Module to validate jobs.
@@ -39,6 +41,8 @@ const {standardize} = require('./procs/standardize');
39
41
  const {identify} = require('./procs/identify');
40
42
  // Module to send a notice to an observer.
41
43
  const {tellServer} = require('./procs/tellServer');
44
+ // Module to create child processes.
45
+ const {fork} = require('child_process');
42
46
 
43
47
  // CONSTANTS
44
48
 
@@ -194,7 +198,7 @@ const browserClose = async () => {
194
198
  }
195
199
  };
196
200
  // Launches a browser, navigates to a URL, and returns browser data.
197
- const launch = async (report, debug, waits, tempBrowserID, tempURL) => {
201
+ const launch = exports.launch = async (report, debug, waits, tempBrowserID, tempURL) => {
198
202
  const act = report.acts[actIndex];
199
203
  const {device} = report;
200
204
  const deviceID = device && device.id;
@@ -306,25 +310,21 @@ const launch = async (report, debug, waits, tempBrowserID, tempURL) => {
306
310
  // Otherwise, i.e. if the launch or navigation failed:
307
311
  else {
308
312
  // Report this and abort the job.
309
- actIndex = await addError(
310
- true, true, report, actIndex, `ERROR: Launch failed (${navResult.error})`
311
- );
313
+ addError(true, true, report, actIndex, `ERROR: Launch failed (${navResult.error})`);
312
314
  page = null;
313
315
  }
314
316
  }
315
317
  // If an error occurred:
316
318
  catch(error) {
317
319
  // Report this and abort the job.
318
- actIndex = await addError(
319
- true, true, report, actIndex, `ERROR launching or navigating ${error.message}`
320
- );
320
+ addError(true, true, report, actIndex, `ERROR launching or navigating ${error.message}`);
321
321
  page = null;
322
322
  };
323
323
  }
324
324
  // Otherwise, i.e. if the browser or device ID is invalid:
325
325
  else {
326
326
  // Report this and abort the job.
327
- actIndex = await addError(
327
+ addError(
328
328
  true,
329
329
  true,
330
330
  report,
@@ -332,6 +332,7 @@ const launch = async (report, debug, waits, tempBrowserID, tempURL) => {
332
332
  `ERROR: Browser ${browserID}, device ${deviceID}, or URL ${url} invalid`
333
333
  );
334
334
  }
335
+ exports.page = page;
335
336
  };
336
337
  // Returns a string representing the date and time.
337
338
  const nowString = () => (new Date()).toISOString().slice(2, 16);
@@ -501,18 +502,16 @@ const wait = ms => {
501
502
  });
502
503
  };
503
504
  // Reports a job being aborted and returns an abortive act index.
504
- const abortActs = async (report, actIndex) => {
505
+ const abortActs = (report, actIndex) => {
505
506
  // Add data on the aborted act to the report.
506
507
  report.jobData.abortTime = nowString();
507
508
  report.jobData.abortedAct = actIndex;
508
509
  report.jobData.aborted = true;
509
510
  // Report that the job is aborted.
510
511
  console.log(`ERROR: Job aborted on act ${actIndex}`);
511
- // Return an abortive act index.
512
- return -2;
513
512
  };
514
513
  // Adds an error result to an act.
515
- const addError = async(alsoLog, alsoAbort, report, actIndex, message) => {
514
+ const addError = (alsoLog, alsoAbort, report, actIndex, message) => {
516
515
  // If the error is to be logged:
517
516
  if (alsoLog) {
518
517
  // Log it.
@@ -524,6 +523,7 @@ const addError = async(alsoLog, alsoAbort, report, actIndex, message) => {
524
523
  act.result.success ??= false;
525
524
  act.result.error ??= message;
526
525
  if (act.type === 'test') {
526
+ act.data ??= {};
527
527
  act.data.success = false;
528
528
  act.data.prevented = true;
529
529
  act.data.error = message;
@@ -532,816 +532,803 @@ const addError = async(alsoLog, alsoAbort, report, actIndex, message) => {
532
532
  }
533
533
  // If the job is to be aborted:
534
534
  if (alsoAbort) {
535
- // Return an abortive act index.
536
- return await abortActs(report, actIndex);
537
- }
538
- // Otherwise, i.e. if the job is not to be aborted:
539
- else {
540
- // Return the current act index.
541
- return actIndex;
535
+ // Add this to the report.
536
+ abortActs(report, actIndex);
542
537
  }
543
538
  };
544
- // Recursively performs the acts in a report.
545
- const doActs = async (report, actIndex) => {
539
+ // Performs the acts in a report.
540
+ const doActs = async (report) => {
546
541
  const {acts} = report;
547
- // If any more acts are to be performed:
548
- if (actIndex > -1 && actIndex < acts.length) {
549
- // Identify the act to be performed.
550
- const act = acts[actIndex];
551
- const {type, which} = act;
552
- const actSuffix = type === 'test' ? ` ${which}` : '';
553
- const message = `>>>> ${type}${actSuffix}`;
554
- // If granular reporting has been specified:
555
- if (report.observe) {
556
- // Notify the observer of the act and log it.
557
- const whichParam = which ? `&which=${which}` : '';
558
- const messageParams = `act=${type}${whichParam}`;
559
- tellServer(report, messageParams, message);
560
- }
561
- // Otherwise, i.e. if granular reporting has not been specified:
562
- else {
563
- // Log the act.
564
- console.log(message);
565
- }
566
- // Increment the count of acts performed.
567
- actCount++;
568
- act.startTime = Date.now();
569
- // If the act is an index changer:
570
- if (type === 'next') {
571
- const condition = act.if;
572
- const logSuffix = condition.length === 3 ? ` ${condition[1]} ${condition[2]}` : '';
573
- console.log(`>> ${condition[0]}${logSuffix}`);
574
- // Identify the act to be checked.
575
- const ifActIndex = report.acts.map(act => act.type !== 'next').lastIndexOf(true);
576
- // Determine whether its jump condition is true.
577
- const truth = isTrue(report.acts[ifActIndex].result, condition);
578
- // Add the result to the act.
579
- act.result = {
580
- property: condition[0],
581
- relation: condition[1],
582
- criterion: condition[2],
583
- value: truth[0],
584
- jumpRequired: truth[1]
585
- };
586
- // If the condition is true:
587
- if (truth[1]) {
588
- // If the performance of acts is to stop:
589
- if (act.jump === 0) {
590
- // Quit.
591
- actIndex = -2;
592
- }
593
- // Otherwise, if there is a numerical jump:
594
- else if (act.jump) {
595
- // Set the act index accordingly.
596
- actIndex += act.jump - 1;
597
- }
598
- // Otherwise, if there is a named next act:
599
- else if (act.next) {
600
- // Set the new index accordingly, or stop if it does not exist.
601
- actIndex = acts.map(act => act.name).indexOf(act.next) - 1;
602
- }
542
+ // Get the standardization specification.
543
+ const standard = report.standard || 'only';
544
+ const reportPath = 'temp/report.json';
545
+ // For each act in the reeport.
546
+ for (const actIndex in acts) {
547
+ // If the job has not been aborted:
548
+ if (report.jobData && ! report.jobData.aborted) {
549
+ let act = acts[actIndex];
550
+ const {type, which} = act;
551
+ const actSuffix = type === 'test' ? ` ${which}` : '';
552
+ const message = `>>>> ${type}${actSuffix}`;
553
+ // If granular reporting has been specified:
554
+ if (report.observe) {
555
+ // Notify the observer of the act and log it.
556
+ const whichParam = which ? `&which=${which}` : '';
557
+ const messageParams = `act=${type}${whichParam}`;
558
+ tellServer(report, messageParams, message);
603
559
  }
604
- }
605
- // Otherwise, if the act is a launch:
606
- else if (type === 'launch') {
607
- // Launch a browser, navigate to a page, and add the result to the act.
608
- await launch(
609
- report,
610
- debug,
611
- waits,
612
- act.browserID || report.browserID || '',
613
- act.target && act.target.url || report.target && report.target.url || ''
614
- );
615
- // If this failed:
616
- if (page.prevented) {
617
- // Add this to the act.
618
- act.prevented = true;
619
- act.error = page.error || '';
620
- }
621
- }
622
- // Otherwise, if the act performs tests of a tool:
623
- else if (act.type === 'test') {
624
- // Add a description of the tool to the act.
625
- act.what = tools[act.which];
626
- // Get the start time of the act.
627
- const startTime = Date.now();
628
- try {
629
- // Get the time limit in seconds for the act.
630
- const timeLimit = timeLimits[act.which] || 15;
631
- // If a new browser is to be launched:
632
- if (act.launch) {
633
- // Launch it, navigate to a URL, and replace the page.
634
- await launch(
635
- report,
636
- debug,
637
- waits,
638
- act.launch.browserID || report.browserID,
639
- act.launch.target && act.launch.target.url || report.target.url
640
- );
641
- }
642
- // If the page has not prevented the tool from testing:
643
- if (! page.prevented) {
644
- // Perform the specified tests of the tool.
645
- const actReport = await require(`./tests/${act.which}`)
646
- .reporter(page, report, actIndex, timeLimit);
647
- // Add the data and result to the act.
648
- act.data = actReport.data;
649
- act.result = actReport.result;
650
- // If the tool reported that the page prevented testing:
651
- if (actReport.data.prevented) {
652
- // Add prevention data to the job data.
653
- report.jobData.preventions[act.which] = act.data.error;
654
- }
655
- }
656
- }
657
- // If the tool invocation failed:
658
- catch(error) {
659
- // Report the failure.
660
- const message = error.message.slice(0, 400);
661
- console.log(`ERROR: Test act ${act.which} failed (${message})`);
662
- act.data ??= {};
663
- act.data.prevented = true;
664
- act.data.error = act.data.error ? `${act.data.error}; ${message}` : message;
665
- };
666
- // Add the elapsed time of the tool to the report.
667
- const time = Math.round((Date.now() - startTime) / 1000);
668
- const {toolTimes} = report.jobData;
669
- if (! toolTimes[act.which]) {
670
- toolTimes[act.which] = 0;
560
+ // Otherwise, i.e. if granular reporting has not been specified:
561
+ else {
562
+ // Log the act.
563
+ console.log(message);
671
564
  }
672
- toolTimes[act.which] += time;
673
- const standard = report.standard || 'only';
674
- // If the act was not prevented and standardization is required:
675
- if (! act.data.prevented && ['also', 'only'].includes(standard)) {
676
- // Initialize the standard result.
677
- act.standardResult = {
678
- totals: [0, 0, 0, 0],
679
- instances: []
565
+ // If the act is an index changer:
566
+ if (type === 'next') {
567
+ const condition = act.if;
568
+ const logSuffix = condition.length === 3 ? ` ${condition[1]} ${condition[2]}` : '';
569
+ console.log(`>> ${condition[0]}${logSuffix}`);
570
+ // Identify the act to be checked.
571
+ const ifActIndex = acts.map(act => act.type !== 'next').lastIndexOf(true);
572
+ // Determine whether its jump condition is true.
573
+ const truth = isTrue(acts[ifActIndex].result, condition);
574
+ // Add the result to the act.
575
+ act.result = {
576
+ property: condition[0],
577
+ relation: condition[1],
578
+ criterion: condition[2],
579
+ value: truth[0],
580
+ jumpRequired: truth[1]
680
581
  };
681
- // Populate it.
682
- standardize(act);
683
- // Add a box ID and a path ID to each of its standard instances if missing.
684
- for (const instance of act.standardResult.instances) {
685
- const elementID = await identify(instance, page);
686
- if (! instance.boxID) {
687
- instance.boxID = elementID ? elementID.boxID : '';
582
+ // If the condition is true:
583
+ if (truth[1]) {
584
+ // If the performance of acts is to stop:
585
+ if (act.jump === 0) {
586
+ // Quit.
587
+ actIndex = -2;
688
588
  }
689
- if (! instance.pathID) {
690
- instance.pathID = elementID ? elementID.pathID : '';
589
+ // Otherwise, if there is a numerical jump:
590
+ else if (act.jump) {
591
+ // Set the act index accordingly.
592
+ actIndex += act.jump - 1;
691
593
  }
692
- };
693
- // If the original-format result is not to be included in the report:
694
- if (standard === 'only') {
695
- // Remove it.
696
- delete act.result;
594
+ // Otherwise, if there is a named next act:
595
+ else if (act.next) {
596
+ // Set the new index accordingly, or stop if it does not exist.
597
+ actIndex = acts.map(act => act.name).indexOf(act.next) - 1;
598
+ }
599
+ }
600
+ }
601
+ // Otherwise, if the act is a launch:
602
+ else if (type === 'launch') {
603
+ // Launch a browser, navigate to a page, and add the result to the act.
604
+ await launch(
605
+ report,
606
+ debug,
607
+ waits,
608
+ act.browserID || report.browserID || '',
609
+ act.target && act.target.url || report.target && report.target.url || ''
610
+ );
611
+ // If this failed:
612
+ if (page.prevented) {
613
+ // Add this to the act.
614
+ act.prevented = true;
615
+ act.error = page.error || '';
697
616
  }
698
617
  }
699
- const expectations = act.expect;
700
- // If the act was not prevented and has expectations:
701
- if (! act.data.prevented && expectations) {
702
- // Initialize whether they were fulfilled.
703
- act.expectations = [];
704
- let failureCount = 0;
705
- // For each expectation:
706
- expectations.forEach(spec => {
707
- // Add the its result to the act.
708
- const truth = isTrue(act, spec);
709
- act.expectations.push({
710
- property: spec[0],
711
- relation: spec[1],
712
- criterion: spec[2],
713
- actual: truth[0],
714
- passed: truth[1]
618
+ // Otherwise, if the act is a test act:
619
+ else if (type === 'test') {
620
+ // Add a description of the tool to the act.
621
+ act.what = tools[act.which];
622
+ // Get the start time of the act.
623
+ const startTime = Date.now();
624
+ // Add it to the act.
625
+ act.startTime = startTime;
626
+ // Save the report.
627
+ let reportJSON = JSON.stringify(report);
628
+ await fs.writeFile(reportPath, reportJSON);
629
+ // Create a process and wait for it to perform the act and add the result to the saved report.
630
+ const actResult = await new Promise(resolve => {
631
+ let closed = false;
632
+ const child = fork(
633
+ 'procs/doTestAct', [actIndex], {timeout: 1000 * timeLimits[act.which] || 15000}
634
+ );
635
+ child.on('message', message => {
636
+ if (! closed) {
637
+ closed = true;
638
+ resolve(message);
639
+ }
640
+ });
641
+ child.on('close', code => {
642
+ if (! closed) {
643
+ closed = true;
644
+ resolve(code);
645
+ }
715
646
  });
716
- if (! truth[1]) {
717
- failureCount++;
718
- }
719
647
  });
720
- act.expectationFailures = failureCount;
721
- }
722
- }
723
- // Otherwise, if a current page exists:
724
- else if (page) {
725
- // If the act is navigation to a url:
726
- if (act.type === 'url') {
727
- // Identify the URL.
728
- const resolved = act.which.replace('__dirname', __dirname);
729
- requestedURL = resolved;
730
- // Visit it and wait until the DOM is loaded.
731
- const navResult = await goTo(report, page, requestedURL, 15000, 'domcontentloaded');
732
- // If the visit succeeded:
733
- if (navResult.success) {
734
- // Add the script nonce, if any, to the act.
735
- const {response} = navResult;
736
- const scriptNonce = getNonce(response);
737
- if (scriptNonce) {
738
- report.jobData.lastScriptNonce = scriptNonce;
739
- }
740
- // Add the resulting URL to the act.
741
- if (! act.result) {
742
- act.result = {};
743
- }
744
- act.result.url = page.url();
745
- // If a prohibited redirection occurred:
746
- if (response.exception === 'badRedirection') {
747
- // Report this and abort the job.
748
- actIndex = await addError(
749
- true, true, report, actIndex, 'ERROR: Navigation illicitly redirected'
750
- );
751
- }
648
+ // Get the revised report.
649
+ reportJSON = await fs.readFile(reportPath, 'utf8');
650
+ report = JSON.parse(reportJSON);
651
+ // Get the revised act.
652
+ act = report.acts[actIndex];
653
+ // If the result is an error code:
654
+ if (typeof actResult === 'number') {
655
+ // Add the error data to the act.
656
+ act.data ??= {};
657
+ act.data.prevented = true;
658
+ act.data.error = actResult;
752
659
  }
753
- // Otherwise, i.e. if the visit failed:
660
+ // Otherwise, i.e. if it is not an error code:
754
661
  else {
755
- // Report this and abort the job.
756
- actIndex = await addError(true, true, report, actIndex, 'ERROR: Visit failed');
662
+ // Add the elapsed time of the tool to the report.
663
+ const time = Math.round((Date.now() - startTime) / 1000);
664
+ const {toolTimes} = report.jobData;
665
+ toolTimes[act.which] ??= 0;
666
+ toolTimes[act.which] += time;
667
+ // If the act was not prevented and standardization is required:
668
+ if (['also', 'only'].includes(standard) && act.data && ! act.data.prevented) {
669
+ // Initialize the standard result.
670
+ act.standardResult = {
671
+ totals: [0, 0, 0, 0],
672
+ instances: []
673
+ };
674
+ // Populate it.
675
+ standardize(act);
676
+ // Add a box ID and a path ID to each of its standard instances if missing.
677
+ for (const instance of act.standardResult.instances) {
678
+ const elementID = await identify(instance, page);
679
+ if (! instance.boxID) {
680
+ instance.boxID = elementID ? elementID.boxID : '';
681
+ }
682
+ if (! instance.pathID) {
683
+ instance.pathID = elementID ? elementID.pathID : '';
684
+ }
685
+ };
686
+ // If the original-format result is not to be included in the report:
687
+ if (standard === 'only') {
688
+ // Remove it.
689
+ delete act.result;
690
+ }
691
+ const expectations = act.expect;
692
+ // If the act was not prevented and has expectations:
693
+ if (expectations && act.data && ! act.data.prevented) {
694
+ // Initialize whether they were fulfilled.
695
+ act.expectations = [];
696
+ let failureCount = 0;
697
+ // For each expectation:
698
+ expectations.forEach(spec => {
699
+ // Add its result to the act.
700
+ const truth = isTrue(act, spec);
701
+ act.expectations.push({
702
+ property: spec[0],
703
+ relation: spec[1],
704
+ criterion: spec[2],
705
+ actual: truth[0],
706
+ passed: truth[1]
707
+ });
708
+ if (! truth[1]) {
709
+ failureCount++;
710
+ }
711
+ });
712
+ act.expectationFailures = failureCount;
713
+ }
714
+ }
757
715
  }
758
716
  }
759
- // Otherwise, if the act is a wait for text:
760
- else if (act.type === 'wait') {
761
- const {what, which} = act;
762
- console.log(`>> ${what}`);
763
- const result = act.result = {};
764
- // If the text is to be the URL:
765
- if (what === 'url') {
766
- // Wait for the URL to be the exact text.
767
- try {
768
- await page.waitForURL(which, {timeout: 15000});
769
- result.found = true;
770
- result.url = page.url();
717
+ // Otherwise, if a current page exists:
718
+ else if (page) {
719
+ // If the act is navigation to a url:
720
+ if (act.type === 'url') {
721
+ // Identify the URL.
722
+ const resolved = act.which.replace('__dirname', __dirname);
723
+ requestedURL = resolved;
724
+ // Visit it and wait until the DOM is loaded.
725
+ const navResult = await goTo(report, page, requestedURL, 15000, 'domcontentloaded');
726
+ // If the visit succeeded:
727
+ if (navResult.success) {
728
+ // Add the script nonce, if any, to the act.
729
+ const {response} = navResult;
730
+ const scriptNonce = getNonce(response);
731
+ if (scriptNonce) {
732
+ report.jobData.lastScriptNonce = scriptNonce;
733
+ }
734
+ // Add the resulting URL to the act.
735
+ if (! act.result) {
736
+ act.result = {};
737
+ }
738
+ act.result.url = page.url();
739
+ // If a prohibited redirection occurred:
740
+ if (response.exception === 'badRedirection') {
741
+ // Report this and abort the job.
742
+ addError(true, true, report, actIndex, 'ERROR: Navigation illicitly redirected');
743
+ }
771
744
  }
772
- // If the wait times out:
773
- catch(error) {
774
- // Quit.
775
- actIndex = await abortActs(report, actIndex);
776
- waitError(page, act, error, 'text in the URL');
745
+ // Otherwise, i.e. if the visit failed:
746
+ else {
747
+ // Report this and abort the job.
748
+ addError(true, true, report, actIndex, 'ERROR: Visit failed');
777
749
  }
778
750
  }
779
- // Otherwise, if the text is to be a substring of the page title:
780
- else if (what === 'title') {
781
- // Wait for the page title to include the text, case-insensitively.
782
- try {
783
- await page.waitForFunction(
784
- text => document
785
- && document.title
786
- && document.title.toLowerCase().includes(text.toLowerCase()),
787
- which,
788
- {
789
- polling: 1000,
790
- timeout: 5000
791
- }
792
- );
793
- result.found = true;
794
- result.title = await page.title();
751
+ // Otherwise, if the act is a wait for text:
752
+ else if (act.type === 'wait') {
753
+ const {what, which} = act;
754
+ console.log(`>> ${what}`);
755
+ const result = act.result = {};
756
+ // If the text is to be the URL:
757
+ if (what === 'url') {
758
+ // Wait for the URL to be the exact text.
759
+ try {
760
+ await page.waitForURL(which, {timeout: 15000});
761
+ result.found = true;
762
+ result.url = page.url();
763
+ }
764
+ // If the wait times out:
765
+ catch(error) {
766
+ // Quit.
767
+ actIndex = await abortActs(report, actIndex);
768
+ waitError(page, act, error, 'text in the URL');
769
+ }
795
770
  }
796
- // If the wait times out:
797
- catch(error) {
798
- // Quit.
799
- actIndex = await abortActs(report, actIndex);
800
- waitError(page, act, error, 'text in the title');
771
+ // Otherwise, if the text is to be a substring of the page title:
772
+ else if (what === 'title') {
773
+ // Wait for the page title to include the text, case-insensitively.
774
+ try {
775
+ await page.waitForFunction(
776
+ text => document
777
+ && document.title
778
+ && document.title.toLowerCase().includes(text.toLowerCase()),
779
+ which,
780
+ {
781
+ polling: 1000,
782
+ timeout: 5000
783
+ }
784
+ );
785
+ result.found = true;
786
+ result.title = await page.title();
787
+ }
788
+ // If the wait times out:
789
+ catch(error) {
790
+ // Quit.
791
+ actIndex = await abortActs(report, actIndex);
792
+ waitError(page, act, error, 'text in the title');
793
+ }
801
794
  }
802
- }
803
- // Otherwise, if the text is to be a substring of the text of the page body:
804
- else if (what === 'body') {
805
- // Wait for the body to include the text, case-insensitively.
806
- try {
807
- await page.waitForFunction(
808
- text => document
809
- && document.body
810
- && document.body.innerText.toLowerCase().includes(text.toLowerCase()),
811
- which,
812
- {
813
- polling: 2000,
814
- timeout: 15000
815
- }
816
- );
817
- result.found = true;
795
+ // Otherwise, if the text is to be a substring of the text of the page body:
796
+ else if (what === 'body') {
797
+ // Wait for the body to include the text, case-insensitively.
798
+ try {
799
+ await page.waitForFunction(
800
+ text => document
801
+ && document.body
802
+ && document.body.innerText.toLowerCase().includes(text.toLowerCase()),
803
+ which,
804
+ {
805
+ polling: 2000,
806
+ timeout: 15000
807
+ }
808
+ );
809
+ result.found = true;
810
+ }
811
+ // If the wait times out:
812
+ catch(error) {
813
+ // Quit.
814
+ actIndex = await abortActs(report, actIndex);
815
+ waitError(page, act, error, 'text in the body');
816
+ }
818
817
  }
818
+ }
819
+ // Otherwise, if the act is a wait for a state:
820
+ else if (act.type === 'state') {
821
+ // Wait for it.
822
+ const stateIndex = ['loaded', 'idle'].indexOf(act.which);
823
+ await page.waitForLoadState(
824
+ ['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 15000][stateIndex]}
825
+ )
819
826
  // If the wait times out:
820
- catch(error) {
821
- // Quit.
822
- actIndex = await abortActs(report, actIndex);
823
- waitError(page, act, error, 'text in the body');
827
+ .catch(async error => {
828
+ // Report this and abort the job.
829
+ console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
830
+ addError(true, true, report, actIndex, `ERROR waiting for page to be ${act.which}`);
831
+ });
832
+ // If the wait succeeded:
833
+ if (actIndex > -2) {
834
+ // Add state data to the report.
835
+ act.result = {
836
+ success: true,
837
+ state: act.which
838
+ };
824
839
  }
825
840
  }
826
- }
827
- // Otherwise, if the act is a wait for a state:
828
- else if (act.type === 'state') {
829
- // Wait for it.
830
- const stateIndex = ['loaded', 'idle'].indexOf(act.which);
831
- await page.waitForLoadState(
832
- ['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 15000][stateIndex]}
833
- )
834
- // If the wait times out:
835
- .catch(async error => {
836
- // Report this and abort the job.
837
- console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
838
- actIndex = await addError(
839
- true, true, report, actIndex, `ERROR waiting for page to be ${act.which}`
840
- );
841
- });
842
- // If the wait succeeded:
843
- if (actIndex > -2) {
844
- // Add state data to the report.
845
- act.result = {
846
- success: true,
847
- state: act.which
841
+ // Otherwise, if the act is a page switch:
842
+ else if (act.type === 'page') {
843
+ // Wait for a page to be created and identify it as current.
844
+ page = await browserContext.waitForEvent('page');
845
+ // Wait until it is idle.
846
+ await page.waitForLoadState('networkidle', {timeout: 15000});
847
+ // Add the resulting URL to the act.
848
+ const result = {
849
+ url: page.url()
848
850
  };
851
+ act.result = result;
849
852
  }
850
- }
851
- // Otherwise, if the act is a page switch:
852
- else if (act.type === 'page') {
853
- // Wait for a page to be created and identify it as current.
854
- page = await browserContext.waitForEvent('page');
855
- // Wait until it is idle.
856
- await page.waitForLoadState('networkidle', {timeout: 15000});
857
- // Add the resulting URL to the act.
858
- const result = {
859
- url: page.url()
860
- };
861
- act.result = result;
862
- }
863
- // Otherwise, if the page has a URL:
864
- else if (page.url() && page.url() !== 'about:blank') {
865
- const url = page.url();
866
- // Add the URL to the act.
867
- act.actualURL = url;
868
- // If the act is a revelation:
869
- if (act.type === 'reveal') {
870
- // Make all elements in the page visible.
871
- await page.$$eval('body *', elements => {
872
- elements.forEach(element => {
873
- const styleDec = window.getComputedStyle(element);
874
- if (styleDec.display === 'none') {
875
- element.style.display = 'initial';
876
- }
877
- if (['hidden', 'collapse'].includes(styleDec.visibility)) {
878
- element.style.visibility = 'inherit';
879
- }
853
+ // Otherwise, if the page has a URL:
854
+ else if (page.url() && page.url() !== 'about:blank') {
855
+ const url = page.url();
856
+ // Add the URL to the act.
857
+ act.actualURL = url;
858
+ // If the act is a revelation:
859
+ if (act.type === 'reveal') {
860
+ // Make all elements in the page visible.
861
+ await page.$$eval('body *', elements => {
862
+ elements.forEach(element => {
863
+ const styleDec = window.getComputedStyle(element);
864
+ if (styleDec.display === 'none') {
865
+ element.style.display = 'initial';
866
+ }
867
+ if (['hidden', 'collapse'].includes(styleDec.visibility)) {
868
+ element.style.visibility = 'inherit';
869
+ }
870
+ });
871
+ act.result = {
872
+ success: true
873
+ };
874
+ })
875
+ .catch(error => {
876
+ console.log(`ERROR making all elements visible (${error.message})`);
877
+ act.result = {
878
+ success: false
879
+ };
880
880
  });
881
- act.result = {
882
- success: true
883
- };
884
- })
885
- .catch(error => {
886
- console.log(`ERROR making all elements visible (${error.message})`);
887
- act.result = {
888
- success: false
889
- };
890
- });
891
- }
892
- // Otherwise, if the act is a move:
893
- else if (moves[act.type]) {
894
- const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
895
- // Try up to 5 times to:
896
- act.result = {found: false};
897
- let selection = {};
898
- let tries = 0;
899
- const slimText = act.which ? debloat(act.which) : '';
900
- while (tries++ < 5 && ! act.result.found) {
901
- if (page) {
902
- // Identify the elements of the specified type.
903
- const selections = await page.$$(selector);
904
- // If there are any:
905
- if (selections.length) {
906
- // If there are enough to make a match possible:
907
- if ((act.index || 0) < selections.length) {
908
- // For each element of the specified type:
909
- let matchCount = 0;
910
- const selectionTexts = [];
911
- for (selection of selections) {
912
- // Add its lower-case text or an empty string to the list of element texts.
913
- const selectionText = slimText ? await textOf(page, selection) : '';
914
- selectionTexts.push(selectionText);
915
- // If its text includes any specified text, case-insensitively:
916
- if (selectionText.includes(slimText)) {
917
- // If the element has the specified index among such elements:
918
- if (matchCount++ === (act.index || 0)) {
919
- // Report it as the matching element and stop checking.
920
- act.result.found = true;
921
- act.result.textSpec = slimText;
922
- act.result.textContent = selectionText;
923
- break;
881
+ }
882
+ // Otherwise, if the act is a move:
883
+ else if (moves[act.type]) {
884
+ const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
885
+ // Try up to 5 times to:
886
+ act.result = {found: false};
887
+ let selection = {};
888
+ let tries = 0;
889
+ const slimText = act.which ? debloat(act.which) : '';
890
+ while (tries++ < 5 && ! act.result.found) {
891
+ if (page) {
892
+ // Identify the elements of the specified type.
893
+ const selections = await page.$$(selector);
894
+ // If there are any:
895
+ if (selections.length) {
896
+ // If there are enough to make a match possible:
897
+ if ((act.index || 0) < selections.length) {
898
+ // For each element of the specified type:
899
+ let matchCount = 0;
900
+ const selectionTexts = [];
901
+ for (selection of selections) {
902
+ // Add its lower-case text or an empty string to the list of element texts.
903
+ const selectionText = slimText ? await textOf(page, selection) : '';
904
+ selectionTexts.push(selectionText);
905
+ // If its text includes any specified text, case-insensitively:
906
+ if (selectionText.includes(slimText)) {
907
+ // If the element has the specified index among such elements:
908
+ if (matchCount++ === (act.index || 0)) {
909
+ // Report it as the matching element and stop checking.
910
+ act.result.found = true;
911
+ act.result.textSpec = slimText;
912
+ act.result.textContent = selectionText;
913
+ break;
914
+ }
915
+ }
916
+ }
917
+ // If no element satisfied the specifications:
918
+ if (! act.result.found) {
919
+ // Add the failure data to the report.
920
+ act.result.success = false;
921
+ act.result.error = 'exhausted';
922
+ act.result.typeElementCount = selections.length;
923
+ if (slimText) {
924
+ act.result.textElementCount = --matchCount;
924
925
  }
926
+ act.result.message = 'Not enough specified elements exist';
927
+ act.result.candidateTexts = selectionTexts;
925
928
  }
926
929
  }
927
- // If no element satisfied the specifications:
928
- if (! act.result.found) {
930
+ // Otherwise, i.e. if there are too few such elements to make a match possible:
931
+ else {
929
932
  // Add the failure data to the report.
930
933
  act.result.success = false;
931
- act.result.error = 'exhausted';
934
+ act.result.error = 'fewer';
932
935
  act.result.typeElementCount = selections.length;
933
- if (slimText) {
934
- act.result.textElementCount = --matchCount;
935
- }
936
- act.result.message = 'Not enough specified elements exist';
937
- act.result.candidateTexts = selectionTexts;
936
+ act.result.message = 'Elements of specified type too few';
938
937
  }
939
938
  }
940
- // Otherwise, i.e. if there are too few such elements to make a match possible:
939
+ // Otherwise, i.e. if there are no elements of the specified type:
941
940
  else {
942
941
  // Add the failure data to the report.
943
942
  act.result.success = false;
944
- act.result.error = 'fewer';
945
- act.result.typeElementCount = selections.length;
946
- act.result.message = 'Elements of specified type too few';
943
+ act.result.error = 'none';
944
+ act.result.typeElementCount = 0;
945
+ act.result.message = 'No elements of specified type found';
947
946
  }
948
947
  }
949
- // Otherwise, i.e. if there are no elements of the specified type:
948
+ // Otherwise, i.e. if the page no longer exists:
950
949
  else {
951
950
  // Add the failure data to the report.
952
951
  act.result.success = false;
953
- act.result.error = 'none';
954
- act.result.typeElementCount = 0;
955
- act.result.message = 'No elements of specified type found';
956
- }
957
- }
958
- // Otherwise, i.e. if the page no longer exists:
959
- else {
960
- // Add the failure data to the report.
961
- act.result.success = false;
962
- act.result.error = 'gone';
963
- act.result.message = 'Page gone';
964
- }
965
- if (! act.result.found) {
966
- await wait(2000);
967
- }
968
- }
969
- // If a match was found:
970
- if (act.result.found) {
971
- // FUNCTION DEFINITION START
972
- // Performs a click or Enter keypress and waits for the network to be idle.
973
- const doAndWait = async isClick => {
974
- // Perform and report the move.
975
- const move = isClick ? 'click' : 'Enter keypress';
976
- try {
977
- await isClick
978
- ? selection.click({timeout: 4000})
979
- : selection.press('Enter', {timeout: 4000});
980
- act.result.success = true;
981
- act.result.move = move;
952
+ act.result.error = 'gone';
953
+ act.result.message = 'Page gone';
982
954
  }
983
- // If the move fails:
984
- catch(error) {
985
- // Add the error result to the act and abort the job.
986
- actIndex = await addError(true, true, report, actIndex, `ERROR: ${move} failed`);
955
+ if (! act.result.found) {
956
+ await wait(2000);
987
957
  }
988
- if (act.result.success) {
958
+ }
959
+ // If a match was found:
960
+ if (act.result.found) {
961
+ // FUNCTION DEFINITION START
962
+ // Performs a click or Enter keypress and waits for the network to be idle.
963
+ const doAndWait = async isClick => {
964
+ // Perform and report the move.
965
+ const move = isClick ? 'click' : 'Enter keypress';
989
966
  try {
990
- await page.context().waitForEvent('networkidle', {timeout: 10000});
991
- act.result.idleTimely = true;
967
+ await isClick
968
+ ? selection.click({timeout: 4000})
969
+ : selection.press('Enter', {timeout: 4000});
970
+ act.result.success = true;
971
+ act.result.move = move;
992
972
  }
973
+ // If the move fails:
993
974
  catch(error) {
994
- console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
995
- act.result.idleTimely = false;
975
+ // Add the error result to the act and abort the job.
976
+ addError(true, true, report, actIndex, `ERROR: ${move} failed`);
996
977
  }
997
- // Add the page URL to the result.
998
- act.result.newURL = page.url();
999
- }
1000
- };
1001
- // FUNCTION DEFINITION END
1002
- // If the move is a button click, perform it.
1003
- if (act.type === 'button') {
1004
- await selection.click({timeout: 3000});
1005
- act.result.success = true;
1006
- act.result.move = 'clicked';
1007
- }
1008
- // Otherwise, if it is checking a radio button or checkbox, perform it.
1009
- else if (['checkbox', 'radio'].includes(act.type)) {
1010
- await selection.waitForElementState('stable', {timeout: 2000})
1011
- .catch(error => {
1012
- console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
1013
- act.result.success = false;
1014
- act.result.error = `ERROR waiting for stable ${act.type}`;
1015
- });
1016
- if (! act.result.error) {
1017
- const isEnabled = await selection.isEnabled();
1018
- if (isEnabled) {
1019
- await selection.check({
1020
- force: true,
1021
- timeout: 2000
1022
- })
1023
- .catch(error => {
1024
- console.log(`ERROR checking ${act.type} (${error.message})`);
1025
- act.result.success = false;
1026
- act.result.error = `ERROR checking ${act.type}`;
1027
- });
1028
- if (! act.result.error) {
1029
- act.result.success = true;
1030
- act.result.move = 'checked';
978
+ if (act.result.success) {
979
+ try {
980
+ await page.context().waitForEvent('networkidle', {timeout: 10000});
981
+ act.result.idleTimely = true;
982
+ }
983
+ catch(error) {
984
+ console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
985
+ act.result.idleTimely = false;
1031
986
  }
987
+ // Add the page URL to the result.
988
+ act.result.newURL = page.url();
1032
989
  }
1033
- else {
1034
- const report = `ERROR: could not check ${act.type} because disabled`;
1035
- console.log(report);
990
+ };
991
+ // FUNCTION DEFINITION END
992
+ // If the move is a button click, perform it.
993
+ if (act.type === 'button') {
994
+ await selection.click({timeout: 3000});
995
+ act.result.success = true;
996
+ act.result.move = 'clicked';
997
+ }
998
+ // Otherwise, if it is checking a radio button or checkbox, perform it.
999
+ else if (['checkbox', 'radio'].includes(act.type)) {
1000
+ await selection.waitForElementState('stable', {timeout: 2000})
1001
+ .catch(error => {
1002
+ console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
1036
1003
  act.result.success = false;
1037
- act.result.error = report;
1004
+ act.result.error = `ERROR waiting for stable ${act.type}`;
1005
+ });
1006
+ if (! act.result.error) {
1007
+ const isEnabled = await selection.isEnabled();
1008
+ if (isEnabled) {
1009
+ await selection.check({
1010
+ force: true,
1011
+ timeout: 2000
1012
+ })
1013
+ .catch(error => {
1014
+ console.log(`ERROR checking ${act.type} (${error.message})`);
1015
+ act.result.success = false;
1016
+ act.result.error = `ERROR checking ${act.type}`;
1017
+ });
1018
+ if (! act.result.error) {
1019
+ act.result.success = true;
1020
+ act.result.move = 'checked';
1021
+ }
1022
+ }
1023
+ else {
1024
+ const report = `ERROR: could not check ${act.type} because disabled`;
1025
+ act.result.success = false;
1026
+ act.result.error = report;
1027
+ }
1038
1028
  }
1039
1029
  }
1040
- }
1041
- // Otherwise, if it is focusing the element, perform it.
1042
- else if (act.type === 'focus') {
1043
- await selection.focus({timeout: 2000});
1044
- act.result.success = true;
1045
- act.result.move = 'focused';
1046
- }
1047
- // Otherwise, if it is clicking a link:
1048
- else if (act.type === 'link') {
1049
- const href = await selection.getAttribute('href');
1050
- const target = await selection.getAttribute('target');
1051
- act.result.href = href || 'NONE';
1052
- act.result.target = target || 'DEFAULT';
1053
- // If the destination is a new page:
1054
- if (target && target !== '_self') {
1055
- // Click the link and wait for the network to be idle.
1056
- doAndWait(true);
1030
+ // Otherwise, if it is focusing the element, perform it.
1031
+ else if (act.type === 'focus') {
1032
+ await selection.focus({timeout: 2000});
1033
+ act.result.success = true;
1034
+ act.result.move = 'focused';
1057
1035
  }
1058
- // Otherwise, i.e. if the destination is in the current page:
1059
- else {
1060
- // Click the link and wait for the resulting navigation.
1061
- try {
1062
- await selection.click({timeout: 5000});
1063
- // Wait for the new content to load.
1064
- await page.waitForLoadState('domcontentloaded', {timeout: 6000});
1065
- act.result.success = true;
1066
- act.result.move = 'clicked';
1067
- act.result.newURL = page.url();
1036
+ // Otherwise, if it is clicking a link:
1037
+ else if (act.type === 'link') {
1038
+ const href = await selection.getAttribute('href');
1039
+ const target = await selection.getAttribute('target');
1040
+ act.result.href = href || 'NONE';
1041
+ act.result.target = target || 'DEFAULT';
1042
+ // If the destination is a new page:
1043
+ if (target && target !== '_self') {
1044
+ // Click the link and wait for the network to be idle.
1045
+ doAndWait(true);
1068
1046
  }
1069
- // If the click or load failed:
1070
- catch(error) {
1071
- // Quit and add failure data to the report.
1072
- console.log(`ERROR clicking link (${errorStart(error)})`);
1073
- act.result.success = false;
1074
- act.result.error = 'unclickable';
1075
- act.result.message = 'ERROR: click or load timed out';
1076
- actIndex = await abortActs(report, actIndex);
1047
+ // Otherwise, i.e. if the destination is in the current page:
1048
+ else {
1049
+ // Click the link and wait for the resulting navigation.
1050
+ try {
1051
+ await selection.click({timeout: 5000});
1052
+ // Wait for the new content to load.
1053
+ await page.waitForLoadState('domcontentloaded', {timeout: 6000});
1054
+ act.result.success = true;
1055
+ act.result.move = 'clicked';
1056
+ act.result.newURL = page.url();
1057
+ }
1058
+ // If the click or load failed:
1059
+ catch(error) {
1060
+ // Quit and add failure data to the report.
1061
+ console.log(`ERROR clicking link (${errorStart(error)})`);
1062
+ act.result.success = false;
1063
+ act.result.error = 'unclickable';
1064
+ act.result.message = 'ERROR: click or load timed out';
1065
+ actIndex = await abortActs(report, actIndex);
1066
+ }
1067
+ // If the link click succeeded:
1068
+ if (! act.result.error) {
1069
+ // Add success data to the report.
1070
+ act.result.success = true;
1071
+ act.result.move = 'clicked';
1072
+ }
1077
1073
  }
1078
- // If the link click succeeded:
1079
- if (! act.result.error) {
1080
- // Add success data to the report.
1081
- act.result.success = true;
1082
- act.result.move = 'clicked';
1074
+ }
1075
+ // Otherwise, if it is selecting an option in a select list, perform it.
1076
+ else if (act.type === 'select') {
1077
+ const options = await selection.$$('option');
1078
+ let optionText = '';
1079
+ if (options && Array.isArray(options) && options.length) {
1080
+ const optionTexts = [];
1081
+ for (const option of options) {
1082
+ const optionText = await option.textContent();
1083
+ optionTexts.push(optionText);
1084
+ }
1085
+ const matchTexts = optionTexts.map(
1086
+ (text, index) => text.includes(act.what) ? index : -1
1087
+ );
1088
+ const index = matchTexts.filter(text => text > -1)[act.index || 0];
1089
+ if (index !== undefined) {
1090
+ await selection.selectOption({index});
1091
+ optionText = optionTexts[index];
1092
+ }
1083
1093
  }
1094
+ act.result.success = true;
1095
+ act.result.move = 'selected';
1096
+ act.result.option = optionText;
1084
1097
  }
1085
- }
1086
- // Otherwise, if it is selecting an option in a select list, perform it.
1087
- else if (act.type === 'select') {
1088
- const options = await selection.$$('option');
1089
- let optionText = '';
1090
- if (options && Array.isArray(options) && options.length) {
1091
- const optionTexts = [];
1092
- for (const option of options) {
1093
- const optionText = await option.textContent();
1094
- optionTexts.push(optionText);
1098
+ // Otherwise, if it is entering text in an input element:
1099
+ else if (['text', 'search'].includes(act.type)) {
1100
+ act.result.attributes = {};
1101
+ const {attributes} = act.result;
1102
+ const type = await selection.getAttribute('type');
1103
+ const label = await selection.getAttribute('aria-label');
1104
+ const labelRefs = await selection.getAttribute('aria-labelledby');
1105
+ attributes.type = type || '';
1106
+ attributes.label = label || '';
1107
+ attributes.labelRefs = labelRefs || '';
1108
+ // If the text contains a placeholder for an environment variable:
1109
+ let {what} = act;
1110
+ if (/__[A-Z]+__/.test(what)) {
1111
+ // Replace it.
1112
+ const envKey = /__([A-Z]+)__/.exec(what)[1];
1113
+ const envValue = process.env[envKey];
1114
+ what = what.replace(/__[A-Z]+__/, envValue);
1095
1115
  }
1096
- const matchTexts = optionTexts.map(
1097
- (text, index) => text.includes(act.what) ? index : -1
1098
- );
1099
- const index = matchTexts.filter(text => text > -1)[act.index || 0];
1100
- if (index !== undefined) {
1101
- await selection.selectOption({index});
1102
- optionText = optionTexts[index];
1116
+ // Enter the text.
1117
+ await selection.type(what);
1118
+ report.jobData.presses += what.length;
1119
+ act.result.success = true;
1120
+ act.result.move = 'entered';
1121
+ // If the input is a search input:
1122
+ if (act.type === 'search') {
1123
+ // Press the Enter key and wait for a network to be idle.
1124
+ doAndWait(false);
1103
1125
  }
1104
1126
  }
1105
- act.result.success = true;
1106
- act.result.move = 'selected';
1107
- act.result.option = optionText;
1108
- }
1109
- // Otherwise, if it is entering text in an input element:
1110
- else if (['text', 'search'].includes(act.type)) {
1111
- act.result.attributes = {};
1112
- const {attributes} = act.result;
1113
- const type = await selection.getAttribute('type');
1114
- const label = await selection.getAttribute('aria-label');
1115
- const labelRefs = await selection.getAttribute('aria-labelledby');
1116
- attributes.type = type || '';
1117
- attributes.label = label || '';
1118
- attributes.labelRefs = labelRefs || '';
1119
- // If the text contains a placeholder for an environment variable:
1120
- let {what} = act;
1121
- if (/__[A-Z]+__/.test(what)) {
1122
- // Replace it.
1123
- const envKey = /__([A-Z]+)__/.exec(what)[1];
1124
- const envValue = process.env[envKey];
1125
- what = what.replace(/__[A-Z]+__/, envValue);
1126
- }
1127
- // Enter the text.
1128
- await selection.type(what);
1129
- report.jobData.presses += what.length;
1130
- act.result.success = true;
1131
- act.result.move = 'entered';
1132
- // If the input is a search input:
1133
- if (act.type === 'search') {
1134
- // Press the Enter key and wait for a network to be idle.
1135
- doAndWait(false);
1127
+ // Otherwise, i.e. if the move is unknown, add the failure to the act.
1128
+ else {
1129
+ // Report the error.
1130
+ const report = 'ERROR: move unknown';
1131
+ act.result.success = false;
1132
+ act.result.error = report;
1136
1133
  }
1137
1134
  }
1138
- // Otherwise, i.e. if the move is unknown, add the failure to the act.
1135
+ // Otherwise, i.e. if no match was found:
1139
1136
  else {
1140
- // Report the error.
1141
- const report = 'ERROR: move unknown';
1137
+ // Quit and add failure data to the report.
1142
1138
  act.result.success = false;
1143
- act.result.error = report;
1144
- console.log(report);
1139
+ act.result.error = 'absent';
1140
+ act.result.message = 'ERROR: specified element not found';
1141
+ console.log('ERROR: Specified element not found');
1142
+ actIndex = await abortActs(report, actIndex);
1145
1143
  }
1146
1144
  }
1147
- // Otherwise, i.e. if no match was found:
1148
- else {
1149
- // Quit and add failure data to the report.
1150
- act.result.success = false;
1151
- act.result.error = 'absent';
1152
- act.result.message = 'ERROR: specified element not found';
1153
- console.log('ERROR: Specified element not found');
1154
- actIndex = await abortActs(report, actIndex);
1155
- }
1156
- }
1157
- // Otherwise, if the act is a keypress:
1158
- else if (act.type === 'press') {
1159
- // Identify the number of times to press the key.
1160
- let times = 1 + (act.again || 0);
1161
- report.jobData.presses += times;
1162
- const key = act.which;
1163
- // Press the key.
1164
- while (times--) {
1165
- await page.keyboard.press(key);
1145
+ // Otherwise, if the act is a keypress:
1146
+ else if (act.type === 'press') {
1147
+ // Identify the number of times to press the key.
1148
+ let times = 1 + (act.again || 0);
1149
+ report.jobData.presses += times;
1150
+ const key = act.which;
1151
+ // Press the key.
1152
+ while (times--) {
1153
+ await page.keyboard.press(key);
1154
+ }
1155
+ const qualifier = act.again ? `${1 + act.again} times` : 'once';
1156
+ act.result = {
1157
+ success: true,
1158
+ message: `pressed ${qualifier}`
1159
+ };
1166
1160
  }
1167
- const qualifier = act.again ? `${1 + act.again} times` : 'once';
1168
- act.result = {
1169
- success: true,
1170
- message: `pressed ${qualifier}`
1171
- };
1172
- }
1173
- // Otherwise, if it is a repetitive keyboard navigation:
1174
- else if (act.type === 'presses') {
1175
- const {navKey, what, which, withItems} = act;
1176
- const matchTexts = which ? which.map(text => debloat(text)) : [];
1177
- // Initialize the loop variables.
1178
- let status = 'more';
1179
- let presses = 0;
1180
- let amountRead = 0;
1181
- let items = [];
1182
- let matchedText;
1183
- // As long as a matching element has not been reached:
1184
- while (status === 'more') {
1185
- // Press the Escape key to dismiss any modal dialog.
1186
- await page.keyboard.press('Escape');
1187
- // Press the specified navigation key.
1188
- await page.keyboard.press(navKey);
1189
- presses++;
1190
- // Identify the newly current element or a failure.
1191
- const currentJSHandle = await page.evaluateHandle(actCount => {
1192
- // Initialize it as the focused element.
1193
- let currentElement = document.activeElement;
1194
- // If it exists in the page:
1195
- if (currentElement && currentElement.tagName !== 'BODY') {
1196
- // Change it, if necessary, to its active descendant.
1197
- if (currentElement.hasAttribute('aria-activedescendant')) {
1198
- currentElement = document.getElementById(
1199
- currentElement.getAttribute('aria-activedescendant')
1200
- );
1201
- }
1202
- // Or change it, if necessary, to its selected option.
1203
- else if (currentElement.tagName === 'SELECT') {
1204
- const currentIndex = Math.max(0, currentElement.selectedIndex);
1205
- const options = currentElement.querySelectorAll('option');
1206
- currentElement = options[currentIndex];
1207
- }
1208
- // Or change it, if necessary, to its active shadow-DOM element.
1209
- else if (currentElement.shadowRoot) {
1210
- currentElement = currentElement.shadowRoot.activeElement;
1211
- }
1212
- // If there is a current element:
1213
- if (currentElement) {
1214
- // If it was already reached within this act:
1215
- if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1216
- // Report the error.
1217
- console.log(`ERROR: ${currentElement.tagName} element reached again`);
1218
- status = 'ERROR';
1219
- return 'ERROR: locallyExhausted';
1161
+ // Otherwise, if it is a repetitive keyboard navigation:
1162
+ else if (act.type === 'presses') {
1163
+ const {navKey, what, which, withItems} = act;
1164
+ const matchTexts = which ? which.map(text => debloat(text)) : [];
1165
+ // Initialize the loop variables.
1166
+ let status = 'more';
1167
+ let presses = 0;
1168
+ let amountRead = 0;
1169
+ let items = [];
1170
+ let matchedText;
1171
+ // As long as a matching element has not been reached:
1172
+ while (status === 'more') {
1173
+ // Press the Escape key to dismiss any modal dialog.
1174
+ await page.keyboard.press('Escape');
1175
+ // Press the specified navigation key.
1176
+ await page.keyboard.press(navKey);
1177
+ presses++;
1178
+ // Identify the newly current element or a failure.
1179
+ const currentJSHandle = await page.evaluateHandle(actCount => {
1180
+ // Initialize it as the focused element.
1181
+ let currentElement = document.activeElement;
1182
+ // If it exists in the page:
1183
+ if (currentElement && currentElement.tagName !== 'BODY') {
1184
+ // Change it, if necessary, to its active descendant.
1185
+ if (currentElement.hasAttribute('aria-activedescendant')) {
1186
+ currentElement = document.getElementById(
1187
+ currentElement.getAttribute('aria-activedescendant')
1188
+ );
1189
+ }
1190
+ // Or change it, if necessary, to its selected option.
1191
+ else if (currentElement.tagName === 'SELECT') {
1192
+ const currentIndex = Math.max(0, currentElement.selectedIndex);
1193
+ const options = currentElement.querySelectorAll('option');
1194
+ currentElement = options[currentIndex];
1195
+ }
1196
+ // Or change it, if necessary, to its active shadow-DOM element.
1197
+ else if (currentElement.shadowRoot) {
1198
+ currentElement = currentElement.shadowRoot.activeElement;
1199
+ }
1200
+ // If there is a current element:
1201
+ if (currentElement) {
1202
+ // If it was already reached within this act:
1203
+ if (currentElement.dataset.pressesReached === actCount.toString(10)) {
1204
+ // Report the error.
1205
+ console.log(`ERROR: ${currentElement.tagName} element reached again`);
1206
+ status = 'ERROR';
1207
+ return 'ERROR: locallyExhausted';
1208
+ }
1209
+ // Otherwise, i.e. if it is newly reached within this act:
1210
+ else {
1211
+ // Mark and return it.
1212
+ currentElement.dataset.pressesReached = actCount;
1213
+ return currentElement;
1214
+ }
1220
1215
  }
1221
- // Otherwise, i.e. if it is newly reached within this act:
1216
+ // Otherwise, i.e. if there is no current element:
1222
1217
  else {
1223
- // Mark and return it.
1224
- currentElement.dataset.pressesReached = actCount;
1225
- return currentElement;
1218
+ // Report the error.
1219
+ status = 'ERROR';
1220
+ return 'noActiveElement';
1226
1221
  }
1227
1222
  }
1228
- // Otherwise, i.e. if there is no current element:
1223
+ // Otherwise, i.e. if there is no focus in the page:
1229
1224
  else {
1230
1225
  // Report the error.
1231
1226
  status = 'ERROR';
1232
- return 'noActiveElement';
1233
- }
1234
- }
1235
- // Otherwise, i.e. if there is no focus in the page:
1236
- else {
1237
- // Report the error.
1238
- status = 'ERROR';
1239
- return 'ERROR: globallyExhausted';
1240
- }
1241
- }, actCount);
1242
- // If the current element exists:
1243
- const currentElement = currentJSHandle.asElement();
1244
- if (currentElement) {
1245
- // Update the data.
1246
- const tagNameJSHandle = await currentElement.getProperty('tagName');
1247
- const tagName = await tagNameJSHandle.jsonValue();
1248
- const text = await textOf(page, currentElement);
1249
- // If the text of the current element was found:
1250
- if (text !== null) {
1251
- const textLength = text.length;
1252
- // If it is non-empty and there are texts to match:
1253
- if (matchTexts.length && textLength) {
1254
- // Identify the matching text.
1255
- matchedText = matchTexts.find(matchText => text.includes(matchText));
1227
+ return 'ERROR: globallyExhausted';
1256
1228
  }
1257
- // Update the item data if required.
1258
- if (withItems) {
1259
- const itemData = {
1260
- tagName,
1261
- text,
1262
- textLength
1263
- };
1264
- if (matchedText) {
1265
- itemData.matchedText = matchedText;
1229
+ }, actCount);
1230
+ // If the current element exists:
1231
+ const currentElement = currentJSHandle.asElement();
1232
+ if (currentElement) {
1233
+ // Update the data.
1234
+ const tagNameJSHandle = await currentElement.getProperty('tagName');
1235
+ const tagName = await tagNameJSHandle.jsonValue();
1236
+ const text = await textOf(page, currentElement);
1237
+ // If the text of the current element was found:
1238
+ if (text !== null) {
1239
+ const textLength = text.length;
1240
+ // If it is non-empty and there are texts to match:
1241
+ if (matchTexts.length && textLength) {
1242
+ // Identify the matching text.
1243
+ matchedText = matchTexts.find(matchText => text.includes(matchText));
1266
1244
  }
1267
- items.push(itemData);
1268
- }
1269
- amountRead += textLength;
1270
- // If there is no text-match failure:
1271
- if (matchedText || ! matchTexts.length) {
1272
- // If the element has any specified tag name:
1273
- if (! what || tagName === what) {
1274
- // Change the status.
1275
- status = 'done';
1276
- // Perform the action.
1277
- const inputText = act.text;
1278
- if (inputText) {
1279
- await page.keyboard.type(inputText);
1280
- presses += inputText.length;
1245
+ // Update the item data if required.
1246
+ if (withItems) {
1247
+ const itemData = {
1248
+ tagName,
1249
+ text,
1250
+ textLength
1251
+ };
1252
+ if (matchedText) {
1253
+ itemData.matchedText = matchedText;
1281
1254
  }
1282
- if (act.action) {
1283
- presses++;
1284
- await page.keyboard.press(act.action);
1285
- await page.waitForLoadState();
1255
+ items.push(itemData);
1256
+ }
1257
+ amountRead += textLength;
1258
+ // If there is no text-match failure:
1259
+ if (matchedText || ! matchTexts.length) {
1260
+ // If the element has any specified tag name:
1261
+ if (! what || tagName === what) {
1262
+ // Change the status.
1263
+ status = 'done';
1264
+ // Perform the action.
1265
+ const inputText = act.text;
1266
+ if (inputText) {
1267
+ await page.keyboard.type(inputText);
1268
+ presses += inputText.length;
1269
+ }
1270
+ if (act.action) {
1271
+ presses++;
1272
+ await page.keyboard.press(act.action);
1273
+ await page.waitForLoadState();
1274
+ }
1286
1275
  }
1287
1276
  }
1288
1277
  }
1278
+ else {
1279
+ status = 'ERROR';
1280
+ }
1289
1281
  }
1282
+ // Otherwise, i.e. if there was a failure:
1290
1283
  else {
1291
- status = 'ERROR';
1284
+ // Update the status.
1285
+ status = await currentJSHandle.jsonValue();
1292
1286
  }
1293
1287
  }
1294
- // Otherwise, i.e. if there was a failure:
1295
- else {
1296
- // Update the status.
1297
- status = await currentJSHandle.jsonValue();
1288
+ // Add the result to the act.
1289
+ act.result = {
1290
+ success: true,
1291
+ status,
1292
+ totals: {
1293
+ presses,
1294
+ amountRead
1295
+ }
1296
+ };
1297
+ if (status === 'done' && matchedText) {
1298
+ act.result.matchedText = matchedText;
1298
1299
  }
1299
- }
1300
- // Add the result to the act.
1301
- act.result = {
1302
- success: true,
1303
- status,
1304
- totals: {
1305
- presses,
1306
- amountRead
1300
+ if (withItems) {
1301
+ act.result.items = items;
1307
1302
  }
1308
- };
1309
- if (status === 'done' && matchedText) {
1310
- act.result.matchedText = matchedText;
1303
+ // Add the totals to the report.
1304
+ report.jobData.presses += presses;
1305
+ report.jobData.amountRead += amountRead;
1311
1306
  }
1312
- if (withItems) {
1313
- act.result.items = items;
1307
+ // Otherwise, i.e. if the act type is unknown:
1308
+ else {
1309
+ // Add the error result to the act and abort the job.
1310
+ addError(true, true, report, actIndex, 'ERROR: Invalid act type');
1314
1311
  }
1315
- // Add the totals to the report.
1316
- report.jobData.presses += presses;
1317
- report.jobData.amountRead += amountRead;
1318
1312
  }
1319
- // Otherwise, i.e. if the act type is unknown:
1313
+ // Otherwise, a page URL is required but does not exist, so:
1320
1314
  else {
1321
- // Add the error result to the act and abort the job.
1322
- actIndex = await addError(true, true, report, actIndex, 'ERROR: Invalid act type');
1315
+ // Add an error result to the act and abort the job.
1316
+ addError(true, true, report, actIndex, 'ERROR: Page has no URL');
1323
1317
  }
1324
1318
  }
1325
- // Otherwise, a page URL is required but does not exist, so:
1319
+ // Otherwise, i.e. if no page exists:
1326
1320
  else {
1327
1321
  // Add an error result to the act and abort the job.
1328
- actIndex = await addError(true, true, report, actIndex, 'ERROR: Page has no URL');
1322
+ addError(true, true, report, actIndex, 'ERROR: No page identified');
1329
1323
  }
1324
+ // Add the end time to the act.
1325
+ act.endTime = Date.now();
1330
1326
  }
1331
- // Otherwise, i.e. if no page exists:
1332
- else {
1333
- // Add an error result to the act and abort the job.
1334
- actIndex = await addError(true, true, report, actIndex, 'ERROR: No page identified');
1335
- }
1336
- act.endTime = Date.now();
1337
- // Perform any remaining acts if not aborted.
1338
- await doActs(report, actIndex + 1);
1339
- }
1340
- // Otherwise, if all acts have been performed and the job succeeded:
1341
- else if (! report.jobData.abortTime) {
1342
- console.log('Acts completed');
1343
- await browserClose();
1344
1327
  }
1328
+ console.log('Acts completed');
1329
+ await browserClose();
1330
+ await fs.rm(reportPath, {force: true});
1331
+ return report;
1345
1332
  };
1346
1333
  /*
1347
1334
  Returns whether an initialized job report is valid and, if so, runs the job and adds the results
@@ -1383,8 +1370,8 @@ exports.doJob = async report => {
1383
1370
  process.exit();
1384
1371
  }
1385
1372
  });
1386
- // Recursively perform the acts.
1387
- await doActs(report, 0, null);
1373
+ // Perform the acts and get the revised report.
1374
+ report = await doActs(report, 0, null);
1388
1375
  // Add the end time and duration to the report.
1389
1376
  const endTime = new Date();
1390
1377
  report.jobData.endTime = nowString();
@@ -1400,4 +1387,5 @@ exports.doJob = async report => {
1400
1387
  report.jobData.toolTimes[item[0]] = item[1];
1401
1388
  });
1402
1389
  }
1390
+ return report;
1403
1391
  };