testaro 41.0.0 → 41.0.2

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