testaro 6.0.4 → 8.0.1
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/do.js +62 -0
- package/high.js +4 -23
- package/package.json +1 -1
- package/run.js +193 -232
- package/watch.js +30 -45
- package/runScript.js +0 -48
package/do.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
do.js
|
|
3
|
+
Invokes Testaro modules with arguments.
|
|
4
|
+
This is the universal module for use of Testaro from a command line.
|
|
5
|
+
Arguments:
|
|
6
|
+
0. function to execute.
|
|
7
|
+
1+. arguments to pass to the function.
|
|
8
|
+
Usage examples:
|
|
9
|
+
node do high script454
|
|
10
|
+
node do watch dir once 30
|
|
11
|
+
node do watch net forever 60
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ########## IMPORTS
|
|
15
|
+
|
|
16
|
+
// Module to keep secrets.
|
|
17
|
+
require('dotenv').config();
|
|
18
|
+
// Function to process a high-level testing request.
|
|
19
|
+
const {runJob} = require('./high');
|
|
20
|
+
// Function to watch for jobs.
|
|
21
|
+
const {cycle} = require('./watch');
|
|
22
|
+
|
|
23
|
+
// ########## CONSTANTS
|
|
24
|
+
|
|
25
|
+
const fn = process.argv[2];
|
|
26
|
+
const fnArgs = process.argv.slice(2);
|
|
27
|
+
const reportDir = process.env.REPORTDIR;
|
|
28
|
+
|
|
29
|
+
// ########## FUNCTIONS
|
|
30
|
+
|
|
31
|
+
// Fulfills a high-level testing request.
|
|
32
|
+
const doHigh = async scriptID => {
|
|
33
|
+
await runJob(scriptID);
|
|
34
|
+
console.log(`Job completed and report ${scriptID}.json saved in ${reportDir}`);
|
|
35
|
+
};
|
|
36
|
+
// Starts a watch.
|
|
37
|
+
const doWatch = async (isDirWatch, isForever, interval) => {
|
|
38
|
+
console.log(
|
|
39
|
+
`Starting a ${isForever ? 'repeating' : 'one-time'} ${isDirWatch ? 'directory' : 'network'} watch`
|
|
40
|
+
);
|
|
41
|
+
await cycle(isDirWatch, isForever, interval);
|
|
42
|
+
console.log('Watching ended');
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ########## OPERATION
|
|
46
|
+
|
|
47
|
+
// Execute the requested function.
|
|
48
|
+
if (fn === 'high' && fnArgs.length === 1) {
|
|
49
|
+
doHigh(fnArgs)
|
|
50
|
+
.then(() => {
|
|
51
|
+
console.log('Execution completed');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
else if (fn === 'watch' && fnArgs.length === 3) {
|
|
55
|
+
doWatch(... fnArgs)
|
|
56
|
+
.then(() => {
|
|
57
|
+
console.log('Execution completed');
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log('ERROR: Invalid statement');
|
|
62
|
+
}
|
package/high.js
CHANGED
|
@@ -17,7 +17,6 @@ const {doJob} = require('./run');
|
|
|
17
17
|
|
|
18
18
|
const scriptDir = process.env.SCRIPTDIR;
|
|
19
19
|
const reportDir = process.env.REPORTDIR;
|
|
20
|
-
const scriptID = process.argv[2];
|
|
21
20
|
|
|
22
21
|
// ########## VARIABLES
|
|
23
22
|
|
|
@@ -27,7 +26,7 @@ let timeLimit = 300;
|
|
|
27
26
|
// ########## FUNCTIONS
|
|
28
27
|
|
|
29
28
|
// Performs a file-based job and writes a report file.
|
|
30
|
-
|
|
29
|
+
exports.runJob = async scriptID => {
|
|
31
30
|
try {
|
|
32
31
|
const scriptJSON = await fs.readFile(`${scriptDir}/${scriptID}.json`, 'utf8');
|
|
33
32
|
const script = JSON.parse(scriptJSON);
|
|
@@ -35,35 +34,17 @@ const runJob = async scriptID => {
|
|
|
35
34
|
if (! script.timeLimit) {
|
|
36
35
|
script.timeLimit = timeLimit;
|
|
37
36
|
}
|
|
38
|
-
//
|
|
39
|
-
const timeStamp = Math.floor((Date.now() - Date.UTC(2022, 1)) / 2000).toString(36);
|
|
40
|
-
// Run the script and record the report with the timestamp as name base.
|
|
41
|
-
const id = `${timeStamp}-${scriptID}`;
|
|
37
|
+
// Run the script and record the report.
|
|
42
38
|
const report = {
|
|
43
|
-
id,
|
|
44
|
-
log: [],
|
|
45
39
|
script,
|
|
46
40
|
acts: []
|
|
47
41
|
};
|
|
48
42
|
await doJob(report);
|
|
49
43
|
const reportJSON = JSON.stringify(report, null, 2);
|
|
50
|
-
await fs.writeFile(`${reportDir}/${
|
|
51
|
-
console.log(`Report ${
|
|
44
|
+
await fs.writeFile(`${reportDir}/${scriptID}.json`, reportJSON);
|
|
45
|
+
console.log(`Report ${scriptID}.json recorded in ${process.env.REPORTDIR}`);
|
|
52
46
|
}
|
|
53
47
|
catch(error) {
|
|
54
48
|
console.log(`ERROR running job (${error.message})\n${error.stack}`);
|
|
55
49
|
}
|
|
56
50
|
};
|
|
57
|
-
|
|
58
|
-
// ########## OPERATION
|
|
59
|
-
|
|
60
|
-
// If this module was called with a scriptID argument:
|
|
61
|
-
if (scriptID) {
|
|
62
|
-
// Run the script and write a report.
|
|
63
|
-
runJob(scriptID);
|
|
64
|
-
}
|
|
65
|
-
// Otherwise, i.e. if it was required by another module:
|
|
66
|
-
else {
|
|
67
|
-
// Export runJob so the other module can call it.
|
|
68
|
-
exports.runJob = runJob;
|
|
69
|
-
}
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
run.js
|
|
3
|
-
|
|
3
|
+
Testaro main utility module.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
// ########## IMPORTS
|
|
@@ -113,14 +113,6 @@ const errorWords = [
|
|
|
113
113
|
// ########## VARIABLES
|
|
114
114
|
|
|
115
115
|
// Facts about the current session.
|
|
116
|
-
let logCount = 0;
|
|
117
|
-
let logSize = 0;
|
|
118
|
-
let errorLogCount = 0;
|
|
119
|
-
let errorLogSize = 0;
|
|
120
|
-
let prohibitedCount = 0;
|
|
121
|
-
let visitTimeoutCount = 0;
|
|
122
|
-
let visitRejectionCount = 0;
|
|
123
|
-
let visitLatency = 0;
|
|
124
116
|
let actCount = 0;
|
|
125
117
|
// Facts about the current browser.
|
|
126
118
|
let browser;
|
|
@@ -241,33 +233,24 @@ const isValidCommand = command => {
|
|
|
241
233
|
return false;
|
|
242
234
|
}
|
|
243
235
|
};
|
|
244
|
-
// Validates a script.
|
|
245
|
-
const isValidScript = script => {
|
|
246
|
-
// Get the script data.
|
|
247
|
-
const {what, strict, commands} = script;
|
|
248
|
-
// Return whether the script is valid:
|
|
249
|
-
return what
|
|
250
|
-
&& typeof strict === 'boolean'
|
|
251
|
-
&& commands
|
|
252
|
-
&& typeof what === 'string'
|
|
253
|
-
&& Array.isArray(commands)
|
|
254
|
-
&& commands[0].type === 'launch'
|
|
255
|
-
&& commands.length > 1
|
|
256
|
-
&& commands[1].type === 'url'
|
|
257
|
-
&& isURL(commands[1].which)
|
|
258
|
-
&& commands.every(command => isValidCommand(command));
|
|
259
|
-
};
|
|
260
|
-
// Validates an initialized reports array.
|
|
261
|
-
const isValidActs = acts => Array.isArray(acts) && ! acts.length;
|
|
262
|
-
// Validates an initialized log array.
|
|
263
|
-
const isValidLog = log => Array.isArray(log) && ! log.length;
|
|
264
236
|
// Validates a report object.
|
|
265
|
-
const isValidReport =
|
|
237
|
+
const isValidReport = report => {
|
|
266
238
|
if (report) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
239
|
+
// Return whether the report is valid.
|
|
240
|
+
const {script, acts} = report;
|
|
241
|
+
const {what, strict, commands} = script;
|
|
242
|
+
return what
|
|
243
|
+
&& typeof strict === 'boolean'
|
|
244
|
+
&& commands
|
|
245
|
+
&& typeof what === 'string'
|
|
246
|
+
&& Array.isArray(commands)
|
|
247
|
+
&& commands[0].type === 'launch'
|
|
248
|
+
&& commands.length > 1
|
|
249
|
+
&& commands[1].type === 'url'
|
|
250
|
+
&& isURL(commands[1].which)
|
|
251
|
+
&& commands.every(command => isValidCommand(command))
|
|
252
|
+
&& Array.isArray(acts)
|
|
253
|
+
&& ! acts.length;
|
|
271
254
|
}
|
|
272
255
|
else {
|
|
273
256
|
return false;
|
|
@@ -276,6 +259,8 @@ const isValidReport = async report => {
|
|
|
276
259
|
|
|
277
260
|
// ########## OTHER FUNCTIONS
|
|
278
261
|
|
|
262
|
+
// Returns a string representing the date and time.
|
|
263
|
+
const nowString = () => (new Date()).toISOString().slice(0, 19);
|
|
279
264
|
// Closes the current browser.
|
|
280
265
|
const browserClose = async () => {
|
|
281
266
|
if (browser) {
|
|
@@ -289,7 +274,7 @@ const browserClose = async () => {
|
|
|
289
274
|
// Returns the first line of an error message.
|
|
290
275
|
const errorStart = error => error.message.replace(/\n.+/s, '');
|
|
291
276
|
// Launches a browser.
|
|
292
|
-
const launch = async typeName => {
|
|
277
|
+
const launch = async (report, typeName) => {
|
|
293
278
|
const browserType = require('playwright')[typeName];
|
|
294
279
|
// If the specified browser type exists:
|
|
295
280
|
if (browserType) {
|
|
@@ -343,15 +328,15 @@ const launch = async typeName => {
|
|
|
343
328
|
console.log(`\n${indentedMsg}`);
|
|
344
329
|
const msgTextLC = msgText.toLowerCase();
|
|
345
330
|
const msgLength = msgText.length;
|
|
346
|
-
logCount++;
|
|
347
|
-
logSize += msgLength;
|
|
331
|
+
report.jobData.logCount++;
|
|
332
|
+
report.jobData.logSize += msgLength;
|
|
348
333
|
if (errorWords.some(word => msgTextLC.includes(word))) {
|
|
349
|
-
errorLogCount++;
|
|
350
|
-
errorLogSize += msgLength;
|
|
334
|
+
report.jobData.errorLogCount++;
|
|
335
|
+
report.jobData.errorLogSize += msgLength;
|
|
351
336
|
}
|
|
352
337
|
const msgLC = msgText.toLowerCase();
|
|
353
338
|
if (msgText.includes('403') && (msgLC.includes('status') || msgLC.includes('prohibited'))) {
|
|
354
|
-
prohibitedCount++;
|
|
339
|
+
report.jobData.prohibitedCount++;
|
|
355
340
|
}
|
|
356
341
|
});
|
|
357
342
|
});
|
|
@@ -459,114 +444,6 @@ const textOf = async (page, element) => {
|
|
|
459
444
|
};
|
|
460
445
|
// Returns a string with any final slash removed.
|
|
461
446
|
const deSlash = string => string.endsWith('/') ? string.slice(0, -1) : string;
|
|
462
|
-
// Tries to visit a URL.
|
|
463
|
-
const goto = async (page, url, timeout, waitUntil, isStrict) => {
|
|
464
|
-
if (url.startsWith('file://.')) {
|
|
465
|
-
url = url.replace('file://', `file://${__dirname}/`);
|
|
466
|
-
}
|
|
467
|
-
// Visit the URL.
|
|
468
|
-
const startTime = Date.now();
|
|
469
|
-
const response = await page.goto(url, {
|
|
470
|
-
timeout,
|
|
471
|
-
waitUntil
|
|
472
|
-
})
|
|
473
|
-
.catch(error => {
|
|
474
|
-
console.log(`ERROR: Visit to ${url} timed out before ${waitUntil} (${errorStart(error)})`);
|
|
475
|
-
visitTimeoutCount++;
|
|
476
|
-
return 'error';
|
|
477
|
-
});
|
|
478
|
-
visitLatency += Math.round((Date.now() - startTime) / 1000);
|
|
479
|
-
// If the visit succeeded:
|
|
480
|
-
if (typeof response !== 'string') {
|
|
481
|
-
const httpStatus = response.status();
|
|
482
|
-
// If the response status was normal:
|
|
483
|
-
if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
|
|
484
|
-
// If the browser was redirected in violation of a strictness requirement.
|
|
485
|
-
const actualURL = page.url();
|
|
486
|
-
if (isStrict && deSlash(actualURL) !== deSlash(url)) {
|
|
487
|
-
// Return an error.
|
|
488
|
-
console.log(`ERROR: Visit to ${url} redirected to ${actualURL}`);
|
|
489
|
-
return 'redirection';
|
|
490
|
-
}
|
|
491
|
-
// Otherwise, i.e. if no prohibited redirection occurred:
|
|
492
|
-
else {
|
|
493
|
-
// Press the Escape key to dismiss any modal dialog.
|
|
494
|
-
await page.keyboard.press('Escape');
|
|
495
|
-
// Return the response.
|
|
496
|
-
return response;
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
// Otherwise, i.e. if the response status was abnormal:
|
|
500
|
-
else {
|
|
501
|
-
// Return an error.
|
|
502
|
-
console.log(`ERROR: Visit to ${url} got status ${httpStatus}`);
|
|
503
|
-
visitRejectionCount++;
|
|
504
|
-
return 'error';
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
// Otherwise, i.e. if the visit failed:
|
|
508
|
-
else {
|
|
509
|
-
// Return an error.
|
|
510
|
-
return 'error';
|
|
511
|
-
}
|
|
512
|
-
};
|
|
513
|
-
// Visits the URL that is the value of the “which” property of an act.
|
|
514
|
-
const visit = async (act, page, isStrict) => {
|
|
515
|
-
// Identify the URL.
|
|
516
|
-
const resolved = act.which.replace('__dirname', __dirname);
|
|
517
|
-
requestedURL = resolved;
|
|
518
|
-
// Visit it and wait until the network is idle.
|
|
519
|
-
let response = await goto(page, requestedURL, 15000, 'networkidle', isStrict);
|
|
520
|
-
// If the visit fails:
|
|
521
|
-
if (response === 'error') {
|
|
522
|
-
// Try again until the DOM is loaded.
|
|
523
|
-
response = await goto(page, requestedURL, 10000, 'domcontentloaded', isStrict);
|
|
524
|
-
// If the visit fails:
|
|
525
|
-
if (response === 'error') {
|
|
526
|
-
// Launch another browser type.
|
|
527
|
-
const newBrowserName = Object.keys(browserTypeNames)
|
|
528
|
-
.find(name => name !== browserTypeName);
|
|
529
|
-
console.log(`>> Launching ${newBrowserName} instead`);
|
|
530
|
-
await launch(newBrowserName);
|
|
531
|
-
// Identify its only page as current.
|
|
532
|
-
page = browserContext.pages()[0];
|
|
533
|
-
// Try again until the network is idle.
|
|
534
|
-
response = await goto(page, requestedURL, 10000, 'networkidle', isStrict);
|
|
535
|
-
// If the visit fails:
|
|
536
|
-
if (response === 'error') {
|
|
537
|
-
// Try again until the DOM is loaded.
|
|
538
|
-
response = await goto(page, requestedURL, 5000, 'domcontentloaded', isStrict);
|
|
539
|
-
// If the visit fails:
|
|
540
|
-
if (response === 'error') {
|
|
541
|
-
// Try again or until a load.
|
|
542
|
-
response = await goto(page, requestedURL, 5000, 'load', isStrict);
|
|
543
|
-
// If the visit fails:
|
|
544
|
-
if (response === 'error') {
|
|
545
|
-
// Give up.
|
|
546
|
-
const errorMsg = `ERROR: Attempts to visit ${requestedURL} failed`;
|
|
547
|
-
console.log(errorMsg);
|
|
548
|
-
act.result = errorMsg;
|
|
549
|
-
await page.goto('about:blank')
|
|
550
|
-
.catch(error => {
|
|
551
|
-
console.log(`ERROR: Navigation to blank page failed (${error.message})`);
|
|
552
|
-
});
|
|
553
|
-
return null;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
// If one of the visits succeeded:
|
|
560
|
-
if (response) {
|
|
561
|
-
// Add the resulting URL to the act.
|
|
562
|
-
if (isStrict && response === 'redirection') {
|
|
563
|
-
act.error = 'ERROR: Navigation redirected';
|
|
564
|
-
}
|
|
565
|
-
act.result = page.url();
|
|
566
|
-
// Return the page.
|
|
567
|
-
return page;
|
|
568
|
-
}
|
|
569
|
-
};
|
|
570
447
|
// Returns a property value and whether it satisfies an expectation.
|
|
571
448
|
const isTrue = (object, specs) => {
|
|
572
449
|
const property = specs[0];
|
|
@@ -634,6 +511,9 @@ const wait = ms => {
|
|
|
634
511
|
};
|
|
635
512
|
// Adds an error result to an act.
|
|
636
513
|
const addError = (act, error, message) => {
|
|
514
|
+
if (! act.result) {
|
|
515
|
+
act.result = {};
|
|
516
|
+
}
|
|
637
517
|
act.result.success = false;
|
|
638
518
|
act.result.error = error;
|
|
639
519
|
act.result.message = message;
|
|
@@ -641,6 +521,65 @@ const addError = (act, error, message) => {
|
|
|
641
521
|
act.result.prevented = true;
|
|
642
522
|
}
|
|
643
523
|
};
|
|
524
|
+
// Visits a URL and returns the response of the server.
|
|
525
|
+
const goTo = async (report, page, url, timeout, waitUntil, isStrict) => {
|
|
526
|
+
if (url.startsWith('file://')) {
|
|
527
|
+
url = url.replace('file://', `file://${__dirname}/`);
|
|
528
|
+
}
|
|
529
|
+
// Visit the URL.
|
|
530
|
+
const startTime = Date.now();
|
|
531
|
+
const response = await page.goto(url, {
|
|
532
|
+
timeout,
|
|
533
|
+
waitUntil
|
|
534
|
+
})
|
|
535
|
+
.catch(error => {
|
|
536
|
+
console.log(`ERROR: Visit to ${url} timed out before ${waitUntil} (${errorStart(error)})`);
|
|
537
|
+
report.jobData.visitTimeoutCount++;
|
|
538
|
+
return {
|
|
539
|
+
error: 'timeout'
|
|
540
|
+
};
|
|
541
|
+
});
|
|
542
|
+
report.jobData.visitLatency += Math.round((Date.now() - startTime) / 1000);
|
|
543
|
+
// If the visit succeeded:
|
|
544
|
+
if (! response.error) {
|
|
545
|
+
const httpStatus = response.status();
|
|
546
|
+
// If the response status was normal:
|
|
547
|
+
if ([200, 304].includes(httpStatus) || url.startsWith('file:')) {
|
|
548
|
+
// If the browser was redirected in violation of a strictness requirement:
|
|
549
|
+
const actualURL = page.url();
|
|
550
|
+
if (isStrict && deSlash(actualURL) !== deSlash(url)) {
|
|
551
|
+
// Return an error.
|
|
552
|
+
console.log(`ERROR: Visit to ${url} redirected to ${actualURL}`);
|
|
553
|
+
return {
|
|
554
|
+
exception: 'badRedirection'
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
// Otherwise, i.e. if no prohibited redirection occurred:
|
|
558
|
+
else {
|
|
559
|
+
// Press the Escape key to dismiss any modal dialog.
|
|
560
|
+
await page.keyboard.press('Escape');
|
|
561
|
+
// Return the response.
|
|
562
|
+
return response;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Otherwise, i.e. if the response status was abnormal:
|
|
566
|
+
else {
|
|
567
|
+
// Return an error.
|
|
568
|
+
console.log(`ERROR: Visit to ${url} got status ${httpStatus}`);
|
|
569
|
+
report.jobData.visitRejectionCount++;
|
|
570
|
+
return {
|
|
571
|
+
error: 'badStatus'
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
// Otherwise, i.e. if the visit failed:
|
|
576
|
+
else {
|
|
577
|
+
// Return an error.
|
|
578
|
+
return {
|
|
579
|
+
error: 'noStatus'
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
};
|
|
644
583
|
// Recursively performs the acts in a report.
|
|
645
584
|
const doActs = async (report, actIndex, page) => {
|
|
646
585
|
process.on('message', message => {
|
|
@@ -700,7 +639,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
700
639
|
// Otherwise, if the command is a launch:
|
|
701
640
|
else if (act.type === 'launch') {
|
|
702
641
|
// Launch the specified browser, creating a browser context and a page in it.
|
|
703
|
-
await launch(act.which);
|
|
642
|
+
await launch(report, act.which);
|
|
704
643
|
// Identify its only page as current.
|
|
705
644
|
page = browserContext.pages()[0];
|
|
706
645
|
}
|
|
@@ -708,8 +647,73 @@ const doActs = async (report, actIndex, page) => {
|
|
|
708
647
|
else if (page) {
|
|
709
648
|
// If the command is a url:
|
|
710
649
|
if (act.type === 'url') {
|
|
711
|
-
//
|
|
712
|
-
|
|
650
|
+
// Identify the URL.
|
|
651
|
+
const resolved = act.which.replace('__dirname', __dirname);
|
|
652
|
+
requestedURL = resolved;
|
|
653
|
+
// Visit it and wait until the network is idle.
|
|
654
|
+
const {strict} = report.script;
|
|
655
|
+
let response = await goTo(report, page, requestedURL, 15000, 'networkidle', strict);
|
|
656
|
+
// If the visit fails:
|
|
657
|
+
if (response.error) {
|
|
658
|
+
// Try again until the DOM is loaded.
|
|
659
|
+
response = await goTo(report, page, requestedURL, 10000, 'domcontentloaded', strict);
|
|
660
|
+
// If the visit fails:
|
|
661
|
+
if (response.error) {
|
|
662
|
+
// Launch another browser type.
|
|
663
|
+
const newBrowserName = Object.keys(browserTypeNames)
|
|
664
|
+
.find(name => name !== browserTypeName);
|
|
665
|
+
console.log(`>> Launching ${newBrowserName} instead`);
|
|
666
|
+
await launch(newBrowserName);
|
|
667
|
+
// Identify its only page as current.
|
|
668
|
+
page = browserContext.pages()[0];
|
|
669
|
+
// Try again until the network is idle.
|
|
670
|
+
response = await goTo(report, page, requestedURL, 10000, 'networkidle', strict);
|
|
671
|
+
// If the visit fails:
|
|
672
|
+
if (response.error) {
|
|
673
|
+
// Try again until the DOM is loaded.
|
|
674
|
+
response = await goTo(report, page, requestedURL, 5000, 'domcontentloaded', strict);
|
|
675
|
+
// If the visit fails:
|
|
676
|
+
if (response.error) {
|
|
677
|
+
// Try again or until a load.
|
|
678
|
+
response = await goTo(report, page, requestedURL, 5000, 'load', strict);
|
|
679
|
+
// If the visit fails:
|
|
680
|
+
if (response.error) {
|
|
681
|
+
// Navigate to a blank page instead.
|
|
682
|
+
await page.goto('about:blank')
|
|
683
|
+
.catch(error => {
|
|
684
|
+
console.log(`ERROR: Navigation to blank page failed (${error.message})`);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// If none of the visits succeeded:
|
|
692
|
+
if (response.error) {
|
|
693
|
+
// Report this.
|
|
694
|
+
report.jobData.aborted = true;
|
|
695
|
+
report.jobData.abortedAct = actIndex;
|
|
696
|
+
addError(act, 'failure', 'ERROR: Visits failed');
|
|
697
|
+
// Quit.
|
|
698
|
+
actIndex = -2;
|
|
699
|
+
}
|
|
700
|
+
// Otherwise, i.e. if the last visit attempt succeeded:
|
|
701
|
+
else {
|
|
702
|
+
// If a prohibited redirection occurred:
|
|
703
|
+
if (response.exception === 'badRedirection') {
|
|
704
|
+
// Report this.
|
|
705
|
+
report.jobData.aborted = true;
|
|
706
|
+
report.jobData.abortedAct = actIndex;
|
|
707
|
+
addError(act, 'badRedirection', 'ERROR: Navigation illicitly redirected');
|
|
708
|
+
// Quit.
|
|
709
|
+
actIndex = -2;
|
|
710
|
+
}
|
|
711
|
+
// Add the resulting URL to the act.
|
|
712
|
+
if (! act.result) {
|
|
713
|
+
act.result = {};
|
|
714
|
+
}
|
|
715
|
+
act.result.url = page.url();
|
|
716
|
+
}
|
|
713
717
|
}
|
|
714
718
|
// Otherwise, if the act is a wait for text:
|
|
715
719
|
else if (act.type === 'wait') {
|
|
@@ -782,10 +786,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
782
786
|
)
|
|
783
787
|
.catch(error => {
|
|
784
788
|
console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
|
|
785
|
-
act
|
|
786
|
-
success: false,
|
|
787
|
-
error: `ERROR waiting for page to be ${act.which}`
|
|
788
|
-
};
|
|
789
|
+
addError(act, 'timeout', `ERROR waiting for page to be ${act.which}`);
|
|
789
790
|
actIndex = -2;
|
|
790
791
|
});
|
|
791
792
|
if (actIndex > -2) {
|
|
@@ -976,8 +977,8 @@ const doActs = async (report, actIndex, page) => {
|
|
|
976
977
|
testReport.result.failureCount = failureCount;
|
|
977
978
|
}
|
|
978
979
|
testReport.result.success = true;
|
|
979
|
-
report.testTimes.push([act.which, Math.round((Date.now() - startTime) / 1000)]);
|
|
980
|
-
report.testTimes.sort((a, b) => b[1] - a[1]);
|
|
980
|
+
report.jobData.testTimes.push([act.which, Math.round((Date.now() - startTime) / 1000)]);
|
|
981
|
+
report.jobData.testTimes.sort((a, b) => b[1] - a[1]);
|
|
981
982
|
// Add the result object (possibly an array) to the act.
|
|
982
983
|
const resultCount = Object.keys(testReport.result).length;
|
|
983
984
|
act.result = resultCount ? testReport.result : {success: false};
|
|
@@ -1220,7 +1221,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1220
1221
|
}
|
|
1221
1222
|
// Enter the text.
|
|
1222
1223
|
await selection.type(act.what);
|
|
1223
|
-
report.presses += act.what.length;
|
|
1224
|
+
report.jobData.presses += act.what.length;
|
|
1224
1225
|
act.result.success = true;
|
|
1225
1226
|
act.result.move = 'entered';
|
|
1226
1227
|
// If the input is a search input:
|
|
@@ -1252,7 +1253,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1252
1253
|
else if (act.type === 'press') {
|
|
1253
1254
|
// Identify the number of times to press the key.
|
|
1254
1255
|
let times = 1 + (act.again || 0);
|
|
1255
|
-
report.presses += times;
|
|
1256
|
+
report.jobData.presses += times;
|
|
1256
1257
|
const key = act.which;
|
|
1257
1258
|
// Press the key.
|
|
1258
1259
|
while (times--) {
|
|
@@ -1407,30 +1408,22 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1407
1408
|
act.result.items = items;
|
|
1408
1409
|
}
|
|
1409
1410
|
// Add the totals to the report.
|
|
1410
|
-
report.presses += presses;
|
|
1411
|
-
report.amountRead += amountRead;
|
|
1411
|
+
report.jobData.presses += presses;
|
|
1412
|
+
report.jobData.amountRead += amountRead;
|
|
1412
1413
|
}
|
|
1413
1414
|
// Otherwise, i.e. if the act type is unknown:
|
|
1414
1415
|
else {
|
|
1415
1416
|
// Add the error result to the act.
|
|
1416
|
-
act
|
|
1417
|
-
success: false,
|
|
1418
|
-
error: 'badType',
|
|
1419
|
-
message: 'ERROR: invalid command type'
|
|
1420
|
-
};
|
|
1417
|
+
addError(act, 'badType', 'ERROR: Invalid command type');
|
|
1421
1418
|
}
|
|
1422
1419
|
}
|
|
1423
1420
|
// Otherwise, i.e. if redirection is prohibited but occurred:
|
|
1424
1421
|
else {
|
|
1425
1422
|
// Add an error result to the act.
|
|
1426
|
-
act
|
|
1427
|
-
success: false,
|
|
1428
|
-
error: 'redirection',
|
|
1429
|
-
message: `ERROR: Page redirected to (${url})`
|
|
1430
|
-
};
|
|
1423
|
+
addError(act, 'redirection', `ERROR: Page redirected to (${url})`);
|
|
1431
1424
|
}
|
|
1432
1425
|
}
|
|
1433
|
-
// Otherwise,
|
|
1426
|
+
// Otherwise, a page URL is required but does not exist, so:
|
|
1434
1427
|
else {
|
|
1435
1428
|
// Add an error result to the act.
|
|
1436
1429
|
addError(act, 'noURL', 'ERROR: Page has no URL');
|
|
@@ -1447,60 +1440,18 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1447
1440
|
else {
|
|
1448
1441
|
// Add an error result to the act.
|
|
1449
1442
|
const errorMsg = `ERROR: Invalid command of type ${act.type}`;
|
|
1450
|
-
act.result = {
|
|
1451
|
-
success: false,
|
|
1452
|
-
error: 'badCommand',
|
|
1453
|
-
message: errorMsg
|
|
1454
|
-
};
|
|
1455
1443
|
console.log(errorMsg);
|
|
1444
|
+
addError(act, 'badCommand', errorMsg);
|
|
1456
1445
|
// Quit.
|
|
1457
1446
|
actIndex = -2;
|
|
1458
1447
|
}
|
|
1459
|
-
// Perform the remaining acts.
|
|
1448
|
+
// Perform the remaining acts unless the performance of acts has been aborted.
|
|
1460
1449
|
await doActs(report, actIndex + 1, page);
|
|
1461
1450
|
}
|
|
1462
1451
|
else {
|
|
1463
1452
|
await browserClose();
|
|
1464
1453
|
}
|
|
1465
1454
|
};
|
|
1466
|
-
// Performs the commands in a script.
|
|
1467
|
-
const doScript = async (report) => {
|
|
1468
|
-
// Reinitialize the log statistics.
|
|
1469
|
-
logCount = 0;
|
|
1470
|
-
logSize = 0;
|
|
1471
|
-
errorLogCount = 0;
|
|
1472
|
-
errorLogSize = 0;
|
|
1473
|
-
prohibitedCount = 0;
|
|
1474
|
-
visitTimeoutCount = 0;
|
|
1475
|
-
visitRejectionCount = 0;
|
|
1476
|
-
// Add the start time to the report.
|
|
1477
|
-
const startTime = new Date();
|
|
1478
|
-
report.startTime = startTime.toISOString().slice(0, 19);
|
|
1479
|
-
// Add initialized properties to the report.
|
|
1480
|
-
report.presses = 0;
|
|
1481
|
-
report.amountRead = 0;
|
|
1482
|
-
report.testTimes = [];
|
|
1483
|
-
// Perform the specified acts.
|
|
1484
|
-
await doActs(report, 0, null);
|
|
1485
|
-
// Add the log statistics to the report.
|
|
1486
|
-
report.logCount = logCount;
|
|
1487
|
-
report.logSize = logSize;
|
|
1488
|
-
report.errorLogCount = errorLogCount;
|
|
1489
|
-
report.errorLogSize = errorLogSize;
|
|
1490
|
-
report.prohibitedCount = prohibitedCount;
|
|
1491
|
-
report.visitTimeoutCount = visitTimeoutCount;
|
|
1492
|
-
report.visitRejectionCount = visitRejectionCount;
|
|
1493
|
-
report.visitLatency = visitLatency;
|
|
1494
|
-
// Add the end time and duration to the report.
|
|
1495
|
-
const endTime = new Date();
|
|
1496
|
-
report.endTime = endTime.toISOString().slice(0, 19);
|
|
1497
|
-
report.elapsedSeconds = Math.floor((endTime - startTime) / 1000);
|
|
1498
|
-
// Add an end time to the log.
|
|
1499
|
-
report.log.push({
|
|
1500
|
-
event: 'endTime',
|
|
1501
|
-
value: ((new Date()).toISOString().slice(0, 19))
|
|
1502
|
-
});
|
|
1503
|
-
};
|
|
1504
1455
|
// Injects launch and url acts into a report where necessary to undo DOM changes.
|
|
1505
1456
|
const injectLaunches = acts => {
|
|
1506
1457
|
let injectMore = true;
|
|
@@ -1552,17 +1503,6 @@ const injectLaunches = acts => {
|
|
|
1552
1503
|
exports.doJob = async report => {
|
|
1553
1504
|
// If the report object is valid:
|
|
1554
1505
|
if(isValidReport(report)) {
|
|
1555
|
-
// Add a start time to the log.
|
|
1556
|
-
report.log.push(
|
|
1557
|
-
{
|
|
1558
|
-
event: 'startTime',
|
|
1559
|
-
value: ((new Date()).toISOString().slice(0, 19))
|
|
1560
|
-
}
|
|
1561
|
-
);
|
|
1562
|
-
// Add a time stamp to the report.
|
|
1563
|
-
report.timeStamp = Math.floor((Date.now() - Date.UTC(2022, 1)) / 2000).toString(36);
|
|
1564
|
-
// Add an ID to the report.
|
|
1565
|
-
report.id = `${report.timeStamp}-${report.script.id}`;
|
|
1566
1506
|
// Add the script commands to the report as its initial acts.
|
|
1567
1507
|
report.acts = JSON.parse(JSON.stringify(report.script.commands));
|
|
1568
1508
|
/*
|
|
@@ -1572,10 +1512,31 @@ exports.doJob = async report => {
|
|
|
1572
1512
|
if (urlInject === 'yes') {
|
|
1573
1513
|
injectLaunches(report.acts);
|
|
1574
1514
|
}
|
|
1575
|
-
//
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1515
|
+
// Add initialized job data to the report.
|
|
1516
|
+
const startTime = new Date();
|
|
1517
|
+
report.jobData = {
|
|
1518
|
+
startTime: nowString(),
|
|
1519
|
+
endTime: '',
|
|
1520
|
+
elapsedSeconds: 0,
|
|
1521
|
+
visitLatency: 0,
|
|
1522
|
+
logCount: 0,
|
|
1523
|
+
logSize: 0,
|
|
1524
|
+
errorLogCount: 0,
|
|
1525
|
+
errorLogSize: 0,
|
|
1526
|
+
prohibitedCount: 0,
|
|
1527
|
+
visitTimeoutCount: 0,
|
|
1528
|
+
visitRejectionCount: 0,
|
|
1529
|
+
aborted: false,
|
|
1530
|
+
abortedAct: null,
|
|
1531
|
+
presses: 0,
|
|
1532
|
+
amountRead: 0,
|
|
1533
|
+
testTimes: []
|
|
1534
|
+
};
|
|
1535
|
+
// Recursively perform the specified acts.
|
|
1536
|
+
await doActs(report, 0, null);
|
|
1537
|
+
// Add the end time and duration to the report.
|
|
1538
|
+
const endTime = new Date();
|
|
1539
|
+
report.jobData.endTime = nowString();
|
|
1540
|
+
report.jobData.elapsedSeconds = Math.floor((endTime - startTime) / 1000);
|
|
1580
1541
|
}
|
|
1581
1542
|
};
|
package/watch.js
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
watch.js
|
|
3
|
-
|
|
4
|
-
Arguments:
|
|
5
|
-
0. Watch type: 'dir' or 'net'.
|
|
6
|
-
1. How long to watch: 'once' or 'forever'.
|
|
7
|
-
2. How often to check in seconds.
|
|
8
|
-
Usage example: node watch dir once 15
|
|
3
|
+
Module for watching for a script and running it when found.
|
|
9
4
|
*/
|
|
10
5
|
|
|
11
6
|
// ########## IMPORTS
|
|
@@ -19,15 +14,10 @@ const {doJob} = require('./run');
|
|
|
19
14
|
|
|
20
15
|
// ########## CONSTANTS
|
|
21
16
|
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const interval = Number.parseInt(process.argv[4]);
|
|
25
|
-
let client;
|
|
26
|
-
if (watchType === 'net') {
|
|
27
|
-
client = require(process.env.PROTOCOL || 'https');
|
|
28
|
-
}
|
|
17
|
+
const protocol = process.env.PROTOCOL || 'https';
|
|
18
|
+
const client = require(protocol);
|
|
29
19
|
const jobURL = process.env.JOB_URL;
|
|
30
|
-
const
|
|
20
|
+
const agent = process.env.AGENT;
|
|
31
21
|
const watchDir = process.env.WATCHDIR;
|
|
32
22
|
const doneDir = process.env.DONEDIR;
|
|
33
23
|
const reportURL = process.env.REPORT_URL;
|
|
@@ -64,7 +54,7 @@ const checkDirJob = async () => {
|
|
|
64
54
|
// Checks for a network job.
|
|
65
55
|
const checkNetJob = async () => {
|
|
66
56
|
const script = await new Promise(resolve => {
|
|
67
|
-
const wholeURL = `${
|
|
57
|
+
const wholeURL = `${protocol}://${jobURL}?agent=${agent}`;
|
|
68
58
|
const request = client.request(wholeURL, response => {
|
|
69
59
|
const chunks = [];
|
|
70
60
|
response.on('data', chunk => {
|
|
@@ -96,7 +86,7 @@ const writeDirReport = async report => {
|
|
|
96
86
|
if (scriptID) {
|
|
97
87
|
try {
|
|
98
88
|
const reportJSON = JSON.stringify(report, null, 2);
|
|
99
|
-
const reportName = `${report.timeStamp}-${scriptID}.json`;
|
|
89
|
+
const reportName = `${report.script.timeStamp}-${scriptID}.json`;
|
|
100
90
|
await fs.writeFile(`${reportDir}/${reportName}`, reportJSON);
|
|
101
91
|
console.log(`Report ${reportName} saved`);
|
|
102
92
|
return true;
|
|
@@ -110,7 +100,7 @@ const writeDirReport = async report => {
|
|
|
110
100
|
// Submits a network report.
|
|
111
101
|
const writeNetReport = async report => {
|
|
112
102
|
const ack = await new Promise(resolve => {
|
|
113
|
-
const wholeURL = `${process.env.PROTOCOL}://${reportURL}?
|
|
103
|
+
const wholeURL = `${process.env.PROTOCOL}://${reportURL}?agent=${agent}`;
|
|
114
104
|
const request = client.request(wholeURL, {method: 'POST'}, response => {
|
|
115
105
|
const chunks = [];
|
|
116
106
|
response.on('data', chunk => {
|
|
@@ -131,14 +121,14 @@ const writeNetReport = async report => {
|
|
|
131
121
|
});
|
|
132
122
|
request.write(JSON.stringify(report, null, 2));
|
|
133
123
|
request.end();
|
|
134
|
-
console.log(`Report ${report.
|
|
124
|
+
console.log(`Report ${report.script.id} submitted`);
|
|
135
125
|
});
|
|
136
126
|
return ack;
|
|
137
127
|
};
|
|
138
128
|
// Archives a job.
|
|
139
129
|
const archiveJob = async script => {
|
|
140
130
|
const scriptJSON = JSON.stringify(script, null, 2);
|
|
141
|
-
await fs.writeFile(`${doneDir}/${script.
|
|
131
|
+
await fs.writeFile(`${doneDir}/${script.id}.json`, scriptJSON);
|
|
142
132
|
await fs.rm(`${watchDir}/${script.id}.json`);
|
|
143
133
|
};
|
|
144
134
|
// Waits.
|
|
@@ -149,25 +139,27 @@ const wait = ms => {
|
|
|
149
139
|
}, ms);
|
|
150
140
|
});
|
|
151
141
|
};
|
|
152
|
-
// Runs a
|
|
153
|
-
|
|
142
|
+
// Runs a job and returns a report.
|
|
143
|
+
exports.runJob = async (script, isDirWatch) => {
|
|
154
144
|
const {id} = script;
|
|
155
145
|
if (id) {
|
|
156
146
|
try {
|
|
157
|
-
// Identify the start time and a time stamp.
|
|
158
|
-
const timeStamp = Math.floor((Date.now() - Date.UTC(2022, 1)) / 2000).toString(36);
|
|
159
|
-
script.timeStamp = timeStamp;
|
|
160
147
|
// Initialize a report.
|
|
161
148
|
const report = {
|
|
162
149
|
log: [],
|
|
163
150
|
script,
|
|
164
151
|
acts: []
|
|
165
152
|
};
|
|
153
|
+
// Run the job, adding to the report.
|
|
166
154
|
await doJob(report);
|
|
167
|
-
|
|
155
|
+
// If a directory was watched:
|
|
156
|
+
if (isDirWatch) {
|
|
157
|
+
// Save the report.
|
|
168
158
|
return await writeDirReport(report);
|
|
169
159
|
}
|
|
160
|
+
// Otherwise, i.e. if the network was watched:
|
|
170
161
|
else {
|
|
162
|
+
// Send the report to the server.
|
|
171
163
|
const ack = await writeNetReport(report);
|
|
172
164
|
if (ack.error) {
|
|
173
165
|
console.log(JSON.stringify(ack, null, 2));
|
|
@@ -193,9 +185,10 @@ const runJob = async script => {
|
|
|
193
185
|
}
|
|
194
186
|
};
|
|
195
187
|
// Checks for a job, performs it, and submits a report, once or repeatedly.
|
|
196
|
-
|
|
188
|
+
exports.cycle = async (isDirWatch, isForever, interval) => {
|
|
197
189
|
const intervalMS = 1000 * Number.parseInt(interval);
|
|
198
190
|
let statusOK = true;
|
|
191
|
+
// Prevent a wait before the first iteration.
|
|
199
192
|
let empty = false;
|
|
200
193
|
console.log(`Watching started with intervals of ${interval} seconds when idle`);
|
|
201
194
|
while (statusOK) {
|
|
@@ -204,32 +197,29 @@ const cycle = async forever => {
|
|
|
204
197
|
}
|
|
205
198
|
// Check for a job.
|
|
206
199
|
let script;
|
|
207
|
-
if (
|
|
200
|
+
if (isDirWatch) {
|
|
208
201
|
script = await checkDirJob();
|
|
209
202
|
}
|
|
210
|
-
else if (watchType === 'net') {
|
|
211
|
-
script = await checkNetJob();
|
|
212
|
-
}
|
|
213
203
|
else {
|
|
214
|
-
script =
|
|
215
|
-
console.log('ERROR: invalid WATCH_TYPE environment variable');
|
|
216
|
-
statusOK = false;
|
|
204
|
+
script = await checkNetJob();
|
|
217
205
|
}
|
|
218
206
|
// If there was one:
|
|
219
207
|
if (script.id) {
|
|
220
|
-
// Run it
|
|
208
|
+
// Run it and save a report.
|
|
221
209
|
console.log(`Running script ${script.id}`);
|
|
222
|
-
statusOK = await runJob(script);
|
|
223
|
-
console.log(`Job ${script.id} finished
|
|
210
|
+
statusOK = await exports.runJob(script, isDirWatch);
|
|
211
|
+
console.log(`Job ${script.id} finished`);
|
|
224
212
|
if (statusOK) {
|
|
225
|
-
// If
|
|
226
|
-
if (
|
|
213
|
+
// If a directory was watched:
|
|
214
|
+
if (isDirWatch) {
|
|
227
215
|
// Archive the script.
|
|
228
216
|
await archiveJob(script);
|
|
229
|
-
console.log(`Script ${script.id}
|
|
217
|
+
console.log(`Script ${script.id} archived`);
|
|
230
218
|
}
|
|
231
219
|
// If watching was specified for only 1 job, stop.
|
|
232
|
-
statusOK =
|
|
220
|
+
statusOK = isForever;
|
|
221
|
+
// Prevent a wait before the next iteration.
|
|
222
|
+
empty = false;
|
|
233
223
|
}
|
|
234
224
|
}
|
|
235
225
|
else {
|
|
@@ -238,8 +228,3 @@ const cycle = async forever => {
|
|
|
238
228
|
}
|
|
239
229
|
console.log('Watching ended');
|
|
240
230
|
};
|
|
241
|
-
|
|
242
|
-
// ########## OPERATION
|
|
243
|
-
|
|
244
|
-
// Start watching, as specified, either forever or until 1 job is run.
|
|
245
|
-
cycle(watchForever);
|
package/runScript.js
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
runScript.js
|
|
3
|
-
Runs a script and writes a report file.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
// ########## IMPORTS
|
|
7
|
-
|
|
8
|
-
const {doJob} = require('./run');
|
|
9
|
-
|
|
10
|
-
// ########## FUNCTIONS
|
|
11
|
-
|
|
12
|
-
// Runs a script and returns the report.
|
|
13
|
-
const runScript = async (id, scriptJSON) => {
|
|
14
|
-
const report = {
|
|
15
|
-
id,
|
|
16
|
-
log: [],
|
|
17
|
-
script: JSON.parse(scriptJSON),
|
|
18
|
-
acts: []
|
|
19
|
-
};
|
|
20
|
-
let reportJSON = JSON.stringify(report, null, 2);
|
|
21
|
-
await doJob(report);
|
|
22
|
-
report.acts.forEach(act => {
|
|
23
|
-
try {
|
|
24
|
-
JSON.stringify(act);
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
console.log(`ERROR: act of type ${act.type} malformatted`);
|
|
28
|
-
act = {
|
|
29
|
-
type: act.type || 'ERROR',
|
|
30
|
-
which: act.which || 'N/A',
|
|
31
|
-
prevented: true,
|
|
32
|
-
error: error.message
|
|
33
|
-
};
|
|
34
|
-
console.log(`act changed to:\n${JSON.stringify(act, null, 2)}`);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
try {
|
|
38
|
-
reportJSON = JSON.stringify(report, null, 2);
|
|
39
|
-
return reportJSON;
|
|
40
|
-
}
|
|
41
|
-
catch(error) {
|
|
42
|
-
console.log(`ERROR: report for host ${id} not JSON (${error.message})`);
|
|
43
|
-
return '';
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// ########## OPERATION
|
|
48
|
-
runScript(... process.argv.slice(2));
|