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/dirWatch.js +2 -7
- package/netWatch.js +71 -78
- package/package.json +1 -1
- package/procs/dateOf.js +47 -0
- package/procs/job.js +268 -0
- package/run.js +697 -964
package/run.js
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
Testaro main utility module.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
//
|
|
28
|
+
// IMPORTS
|
|
29
29
|
|
|
30
30
|
// Module to keep secrets.
|
|
31
31
|
require('dotenv').config();
|
|
32
|
-
//
|
|
33
|
-
const {
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
//
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
//
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
//
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
if
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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
|
|
882
|
-
else
|
|
883
|
-
//
|
|
884
|
-
|
|
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
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
|
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
|
|
684
|
+
// Otherwise, i.e. if the visit failed:
|
|
908
685
|
else {
|
|
909
|
-
//
|
|
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
|
|
916
|
-
else if (
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
//
|
|
946
|
-
|
|
947
|
-
//
|
|
948
|
-
actIndex = await
|
|
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
|
|
952
|
-
else if (
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
//
|
|
996
|
-
|
|
997
|
-
//
|
|
998
|
-
|
|
999
|
-
|
|
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
|
|
1020
|
-
else if (
|
|
1021
|
-
// Wait for
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
.
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
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
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
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
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
908
|
+
if (! truth[1]) {
|
|
909
|
+
failureCount++;
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
act.expectationFailures = failureCount;
|
|
1175
913
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
-
//
|
|
1225
|
-
|
|
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 = '
|
|
954
|
+
act.result.error = 'exhausted';
|
|
1229
955
|
act.result.typeElementCount = selections.length;
|
|
1230
|
-
|
|
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
|
|
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 = '
|
|
1238
|
-
act.result.typeElementCount =
|
|
1239
|
-
act.result.message = '
|
|
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
|
|
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 = '
|
|
1247
|
-
act.result.
|
|
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
|
-
//
|
|
1254
|
-
|
|
1255
|
-
//
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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
|
|
1262
|
-
|
|
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
|
-
|
|
1270
|
-
|
|
1017
|
+
console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
|
|
1018
|
+
act.result.idleTimely = false;
|
|
1271
1019
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
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 =
|
|
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
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
|
1333
|
-
else
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
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
|
-
//
|
|
1344
|
-
|
|
1345
|
-
//
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
const
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
act.
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
|
1162
|
+
// Otherwise, i.e. if the move is unknown, add the failure to the act.
|
|
1433
1163
|
else {
|
|
1434
|
-
//
|
|
1164
|
+
// Report the error.
|
|
1165
|
+
const report = 'ERROR: move unknown';
|
|
1435
1166
|
act.result.success = false;
|
|
1436
|
-
act.result.error =
|
|
1437
|
-
|
|
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
|
|
1443
|
-
else
|
|
1444
|
-
//
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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 '
|
|
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
|
|
1252
|
+
// Otherwise, i.e. if there is no current element:
|
|
1521
1253
|
else {
|
|
1522
1254
|
// Report the error.
|
|
1523
1255
|
status = 'ERROR';
|
|
1524
|
-
return '
|
|
1256
|
+
return 'noActiveElement';
|
|
1525
1257
|
}
|
|
1526
|
-
}
|
|
1527
|
-
//
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
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
|
-
|
|
1582
|
-
status = await currentJSHandle.jsonValue();
|
|
1315
|
+
status = 'ERROR';
|
|
1583
1316
|
}
|
|
1584
1317
|
}
|
|
1585
|
-
//
|
|
1586
|
-
|
|
1587
|
-
|
|
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
|
-
|
|
1598
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1332
|
+
};
|
|
1333
|
+
if (status === 'done' && matchedText) {
|
|
1334
|
+
act.result.matchedText = matchedText;
|
|
1603
1335
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
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,
|
|
1343
|
+
// Otherwise, i.e. if the act type is unknown:
|
|
1611
1344
|
else {
|
|
1612
|
-
// Add
|
|
1613
|
-
actIndex = await addError(true, true, report, actIndex, 'ERROR:
|
|
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,
|
|
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:
|
|
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
|
|
1355
|
+
// Otherwise, i.e. if no page exists:
|
|
1624
1356
|
else {
|
|
1625
|
-
// Add error
|
|
1626
|
-
addError(true, true, report, actIndex,
|
|
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 =
|
|
1378
|
+
const reportInvalidity = isValidJob(report);
|
|
1646
1379
|
if (reportInvalidity) {
|
|
1647
1380
|
console.log(`ERROR: ${reportInvalidity}`);
|
|
1648
1381
|
jobData.aborted = true;
|