testaro 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands.js +2 -2
- package/package.json +1 -1
- package/run.js +153 -145
package/commands.js
CHANGED
|
@@ -100,8 +100,8 @@ exports.commands = {
|
|
|
100
100
|
state: [
|
|
101
101
|
'Wait until the page reaches a load state',
|
|
102
102
|
{
|
|
103
|
-
which: [true, 'string', '
|
|
104
|
-
what: [false, 'string', '
|
|
103
|
+
which: [true, 'string', 'isState', '“loaded” or “idle”'],
|
|
104
|
+
what: [false, 'string', 'hasLength', 'comment']
|
|
105
105
|
}
|
|
106
106
|
],
|
|
107
107
|
tenonRequest: [
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -256,7 +256,7 @@ const launch = async typeName => {
|
|
|
256
256
|
// Make its console messages appear in the Playwright console.
|
|
257
257
|
page.on('console', msg => {
|
|
258
258
|
const msgText = msg.text();
|
|
259
|
-
console.log(msgText);
|
|
259
|
+
console.log(`[${msgText}]`);
|
|
260
260
|
logCount++;
|
|
261
261
|
logSize += msgText.length;
|
|
262
262
|
const msgLC = msgText.toLowerCase();
|
|
@@ -369,69 +369,79 @@ const textOf = async (page, element) => {
|
|
|
369
369
|
return null;
|
|
370
370
|
}
|
|
371
371
|
};
|
|
372
|
-
// Returns an element case-insensitively matching a text.
|
|
372
|
+
// Returns an element of a type case-insensitively matching a text.
|
|
373
373
|
const matchElement = async (page, selector, matchText, index = 0) => {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const textInBodyJSHandle = await page.waitForFunction(
|
|
381
|
-
args => {
|
|
382
|
-
const matchText = args[0];
|
|
383
|
-
const bodyText = args[1];
|
|
384
|
-
return ! matchText || bodyText.includes(matchText);
|
|
385
|
-
},
|
|
386
|
-
[slimText, slimBody],
|
|
387
|
-
{timeout: 4000}
|
|
388
|
-
)
|
|
389
|
-
.catch(async error => {
|
|
390
|
-
console.log(`ERROR: text “${matchText}” not in body (${error.message})`);
|
|
391
|
-
});
|
|
392
|
-
// If there is no text to be matched or the body contained it:
|
|
393
|
-
if (textInBodyJSHandle) {
|
|
394
|
-
const lcText = matchText ? matchText.toLowerCase() : '';
|
|
395
|
-
// Identify the selected elements.
|
|
396
|
-
const selections = await page.$$(`body ${selector}`);
|
|
374
|
+
if (matchText) {
|
|
375
|
+
// If the page still exists:
|
|
376
|
+
if (page) {
|
|
377
|
+
const slimText = debloat(matchText);
|
|
378
|
+
// Identify the elements of the specified type.
|
|
379
|
+
const selections = await page.$$(selector);
|
|
397
380
|
// If there are any:
|
|
398
381
|
if (selections.length) {
|
|
399
382
|
// If there are enough to make a match possible:
|
|
400
383
|
if (index < selections.length) {
|
|
401
|
-
// Return the
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
for (const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
if (
|
|
408
|
-
|
|
384
|
+
// Return the specified one, if any.
|
|
385
|
+
let matchCount = 0;
|
|
386
|
+
const selectionTexts = [];
|
|
387
|
+
for (const selection of selections) {
|
|
388
|
+
const selectionText = await textOf(page, selection);
|
|
389
|
+
selectionTexts.push(selectionText);
|
|
390
|
+
if (selectionText.includes(slimText)) {
|
|
391
|
+
if (matchCount++ === index) {
|
|
392
|
+
return {
|
|
393
|
+
success: true,
|
|
394
|
+
matchingElement: selection
|
|
395
|
+
};
|
|
396
|
+
}
|
|
409
397
|
}
|
|
410
398
|
}
|
|
411
|
-
return
|
|
399
|
+
// None satisfied the specifications, so return a failure.
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
error: 'exhausted',
|
|
403
|
+
message: `Text found in only ${matchCount} (not ${index + 1}) of ${selections.length}`,
|
|
404
|
+
candidateTexts: selectionTexts
|
|
405
|
+
};
|
|
412
406
|
}
|
|
413
|
-
// Otherwise, i.e. if there are too few to make a match possible:
|
|
407
|
+
// Otherwise, i.e. if there are too few such elements to make a match possible:
|
|
414
408
|
else {
|
|
415
|
-
// Return
|
|
416
|
-
return
|
|
409
|
+
// Return a failure.
|
|
410
|
+
return {
|
|
411
|
+
success: false,
|
|
412
|
+
error: 'fewer',
|
|
413
|
+
message: `Count of '${selector}' elements only ${selections.length}`
|
|
414
|
+
};
|
|
417
415
|
}
|
|
418
416
|
}
|
|
419
|
-
// Otherwise, i.e. if there are no
|
|
417
|
+
// Otherwise, i.e. if there are no elements of the specified type:
|
|
420
418
|
else {
|
|
421
|
-
|
|
419
|
+
// Return a failure.
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
error: 'none',
|
|
423
|
+
message: `No '${selector}' elements found`
|
|
424
|
+
};
|
|
422
425
|
}
|
|
423
426
|
}
|
|
424
|
-
// Otherwise, i.e. if the
|
|
427
|
+
// Otherwise, i.e. if the page no longer exists:
|
|
425
428
|
else {
|
|
426
|
-
// Return
|
|
427
|
-
return
|
|
429
|
+
// Return a failure.
|
|
430
|
+
return {
|
|
431
|
+
success: false,
|
|
432
|
+
error: 'gone',
|
|
433
|
+
message: 'Page gone'
|
|
434
|
+
};
|
|
428
435
|
}
|
|
429
436
|
}
|
|
430
|
-
// Otherwise, i.e. if
|
|
437
|
+
// Otherwise, i.e. if no text was specified:
|
|
431
438
|
else {
|
|
432
|
-
// Return
|
|
433
|
-
|
|
434
|
-
|
|
439
|
+
// Return a failure.
|
|
440
|
+
return {
|
|
441
|
+
success: false,
|
|
442
|
+
error: 'text',
|
|
443
|
+
message: 'No text specified'
|
|
444
|
+
};
|
|
435
445
|
}
|
|
436
446
|
};
|
|
437
447
|
// Returns a string with any final slash removed.
|
|
@@ -502,8 +512,9 @@ const visit = async (act, page, isStrict) => {
|
|
|
502
512
|
// If the visit fails:
|
|
503
513
|
if (response === 'error') {
|
|
504
514
|
// Give up.
|
|
505
|
-
|
|
506
|
-
|
|
515
|
+
const errorMsg = `ERROR: Attemts to visit ${requestedURL} failed`;
|
|
516
|
+
console.log(errorMsg);
|
|
517
|
+
act.result = errorMsg;
|
|
507
518
|
await page.goto('about:blank')
|
|
508
519
|
.catch(error => {
|
|
509
520
|
console.log(`ERROR: Navigation to blank page failed (${error.message})`);
|
|
@@ -563,6 +574,14 @@ const waitError = (page, act, error, what) => {
|
|
|
563
574
|
act.result.error = `ERROR waiting for ${what}`;
|
|
564
575
|
return false;
|
|
565
576
|
};
|
|
577
|
+
// Waits.
|
|
578
|
+
const wait = ms => {
|
|
579
|
+
return new Promise(resolve => {
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
resolve('');
|
|
582
|
+
}, ms);
|
|
583
|
+
});
|
|
584
|
+
};
|
|
566
585
|
// Recursively performs the acts in a report.
|
|
567
586
|
const doActs = async (report, actIndex, page) => {
|
|
568
587
|
const {acts} = report;
|
|
@@ -597,12 +616,12 @@ const doActs = async (report, actIndex, page) => {
|
|
|
597
616
|
if (truth[1]) {
|
|
598
617
|
// If the performance of commands is to stop:
|
|
599
618
|
if (act.jump === 0) {
|
|
600
|
-
// Set the
|
|
619
|
+
// Set the act index to cause a stop.
|
|
601
620
|
actIndex = -2;
|
|
602
621
|
}
|
|
603
622
|
// Otherwise, if there is a numerical jump:
|
|
604
623
|
else if (act.jump) {
|
|
605
|
-
// Set the
|
|
624
|
+
// Set the act index accordingly.
|
|
606
625
|
actIndex += act.jump - 1;
|
|
607
626
|
}
|
|
608
627
|
// Otherwise, if there is a named next command:
|
|
@@ -630,76 +649,65 @@ const doActs = async (report, actIndex, page) => {
|
|
|
630
649
|
else if (act.type === 'wait') {
|
|
631
650
|
const {what, which} = act;
|
|
632
651
|
console.log(`>> for ${what} to include “${which}”`);
|
|
633
|
-
// Wait 5 or 10 seconds for the specified text, and quit if it does not.
|
|
634
|
-
let successJSHandle;
|
|
652
|
+
// Wait 5 or 10 seconds for the specified text, and quit if it does not appear.
|
|
635
653
|
if (act.what === 'url') {
|
|
636
|
-
|
|
637
|
-
text => document.URL.includes(text), act.which, {timeout: 5000}
|
|
638
|
-
)
|
|
654
|
+
await page.waitForURL(act.which, {timeout: 15000})
|
|
639
655
|
.catch(error => {
|
|
640
|
-
actIndex =
|
|
641
|
-
|
|
656
|
+
actIndex = -2;
|
|
657
|
+
waitError(page, act, error, 'URL');
|
|
642
658
|
});
|
|
643
659
|
}
|
|
644
660
|
else if (act.what === 'title') {
|
|
645
|
-
|
|
646
|
-
text => document.title.includes(text),
|
|
661
|
+
await page.waitForFunction(
|
|
662
|
+
text => document && document.title && document.title.includes(text),
|
|
663
|
+
act.which,
|
|
664
|
+
{
|
|
665
|
+
polling: 1000,
|
|
666
|
+
timeout: 5000
|
|
667
|
+
}
|
|
647
668
|
)
|
|
648
669
|
.catch(error => {
|
|
649
|
-
actIndex =
|
|
650
|
-
|
|
670
|
+
actIndex = -2;
|
|
671
|
+
waitError(page, act, error, 'title');
|
|
651
672
|
});
|
|
652
673
|
}
|
|
653
674
|
else if (act.what === 'body') {
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
actIndex = acts.length;
|
|
662
|
-
console.log('ERROR finding document body');
|
|
663
|
-
return false;
|
|
664
|
-
}
|
|
665
|
-
}, which, {timeout: 20000}
|
|
675
|
+
await page.waitForFunction(
|
|
676
|
+
text => document && document.body && document.body.innerText.includes(text),
|
|
677
|
+
act.which,
|
|
678
|
+
{
|
|
679
|
+
polling: 2000,
|
|
680
|
+
timeout: 10000
|
|
681
|
+
}
|
|
666
682
|
)
|
|
667
|
-
.catch(error => {
|
|
668
|
-
actIndex =
|
|
669
|
-
|
|
683
|
+
.catch(async error => {
|
|
684
|
+
actIndex = -2;
|
|
685
|
+
waitError(page, act, error, 'body');
|
|
670
686
|
});
|
|
671
687
|
}
|
|
672
|
-
|
|
688
|
+
// If the text was found:
|
|
689
|
+
if (actIndex > -2) {
|
|
690
|
+
// Add this to the report.
|
|
673
691
|
act.result = {url: page.url()};
|
|
674
692
|
if (act.what === 'title') {
|
|
675
693
|
act.result.title = await page.title();
|
|
676
694
|
}
|
|
677
|
-
await page.waitForLoadState('networkidle', {timeout: 10000})
|
|
678
|
-
.catch(error => {
|
|
679
|
-
console.log(`ERROR waiting for stability after ${act.what} (${error.message})`);
|
|
680
|
-
act.result.error = `ERROR waiting for stability after ${act.what}`;
|
|
681
|
-
});
|
|
682
695
|
}
|
|
683
696
|
}
|
|
684
697
|
// Otherwise, if the act is a wait for a state:
|
|
685
698
|
else if (act.type === 'state') {
|
|
686
|
-
//
|
|
699
|
+
// Wait for it, and quit if it does not appear.
|
|
687
700
|
const stateIndex = ['loaded', 'idle'].indexOf(act.which);
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
)
|
|
693
|
-
.
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
console.log('ERROR: invalid state');
|
|
701
|
-
act.result = 'ERROR: invalid state';
|
|
702
|
-
actIndex = acts.length;
|
|
701
|
+
await page.waitForLoadState(
|
|
702
|
+
['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 5000][stateIndex]}
|
|
703
|
+
)
|
|
704
|
+
.catch(error => {
|
|
705
|
+
console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
|
|
706
|
+
act.result = `ERROR waiting for page to be ${act.which}`;
|
|
707
|
+
actIndex = -2;
|
|
708
|
+
});
|
|
709
|
+
if (actIndex > -2) {
|
|
710
|
+
act.result = `Page became ${act.which}`;
|
|
703
711
|
}
|
|
704
712
|
}
|
|
705
713
|
// Otherwise, if the act is a page switch:
|
|
@@ -889,48 +897,34 @@ const doActs = async (report, actIndex, page) => {
|
|
|
889
897
|
// Otherwise, if the act is a move:
|
|
890
898
|
else if (moves[act.type]) {
|
|
891
899
|
const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
|
|
892
|
-
//
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
candidateTexts: whichElement
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
// Otherwise, if the body did not contain the text:
|
|
904
|
-
else if (whichElement === -1) {
|
|
905
|
-
// Add the failure to the act.
|
|
906
|
-
act.result = 'ERROR: body did not contain text to match';
|
|
907
|
-
}
|
|
908
|
-
// Otherwise, if there were not enough candidates:
|
|
909
|
-
else if (typeof whichElement === 'number') {
|
|
910
|
-
// Add the failure to the act.
|
|
911
|
-
act.result = {
|
|
912
|
-
candidateCount: whichElement,
|
|
913
|
-
error: 'ERROR: too few such elements to allow a match'
|
|
914
|
-
};
|
|
900
|
+
// Wait 10 seconds until the element to perform the move on is identified.
|
|
901
|
+
let matchResult = {success: false};
|
|
902
|
+
let tries = 0;
|
|
903
|
+
while (tries++ < 5 && ! matchResult.success) {
|
|
904
|
+
matchResult = await matchElement(page, selector, act.which || '', act.index);
|
|
905
|
+
if (! matchResult.success) {
|
|
906
|
+
await wait(2000);
|
|
907
|
+
}
|
|
915
908
|
}
|
|
916
|
-
//
|
|
917
|
-
|
|
909
|
+
// If a match was found:
|
|
910
|
+
if (matchResult.success) {
|
|
911
|
+
const {matchingElement} = matchResult;
|
|
918
912
|
// If the move is a button click, perform it.
|
|
919
913
|
if (act.type === 'button') {
|
|
920
|
-
await
|
|
914
|
+
await matchingElement.click({timeout: 3000});
|
|
921
915
|
act.result = 'clicked';
|
|
922
916
|
}
|
|
923
917
|
// Otherwise, if it is checking a radio button or checkbox, perform it.
|
|
924
918
|
else if (['checkbox', 'radio'].includes(act.type)) {
|
|
925
|
-
await
|
|
919
|
+
await matchingElement.waitForElementState('stable', {timeout: 2000})
|
|
926
920
|
.catch(error => {
|
|
927
921
|
console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
|
|
928
922
|
act.result = `ERROR waiting for stable ${act.type}`;
|
|
929
923
|
});
|
|
930
924
|
if (! act.result) {
|
|
931
|
-
const isEnabled = await
|
|
925
|
+
const isEnabled = await matchingElement.isEnabled();
|
|
932
926
|
if (isEnabled) {
|
|
933
|
-
await
|
|
927
|
+
await matchingElement.check({
|
|
934
928
|
force: true,
|
|
935
929
|
timeout: 2000
|
|
936
930
|
})
|
|
@@ -951,29 +945,37 @@ const doActs = async (report, actIndex, page) => {
|
|
|
951
945
|
}
|
|
952
946
|
// Otherwise, if it is focusing the element, perform it.
|
|
953
947
|
else if (act.type === 'focus') {
|
|
954
|
-
await
|
|
948
|
+
await matchingElement.focus({timeout: 2000});
|
|
955
949
|
act.result = 'focused';
|
|
956
950
|
}
|
|
957
951
|
// Otherwise, if it is clicking a link, perform it.
|
|
958
952
|
else if (act.type === 'link') {
|
|
959
|
-
const href = await
|
|
960
|
-
const target = await
|
|
961
|
-
await
|
|
953
|
+
const href = await matchingElement.getAttribute('href');
|
|
954
|
+
const target = await matchingElement.getAttribute('target');
|
|
955
|
+
await matchingElement.click({timeout: 2000})
|
|
962
956
|
.catch(async () => {
|
|
963
|
-
|
|
957
|
+
console.log('ERROR: First attempt to click link timed out');
|
|
958
|
+
await matchingElement.click({
|
|
964
959
|
force: true,
|
|
965
960
|
timeout: 10000
|
|
961
|
+
})
|
|
962
|
+
.catch(() => {
|
|
963
|
+
actIndex = -2;
|
|
964
|
+
console.log('ERROR: Second (forced) attempt to click link timed out');
|
|
965
|
+
act.result = 'ERROR: Normal and forced click attempts timed out';
|
|
966
966
|
});
|
|
967
967
|
});
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
968
|
+
if (actIndex > -2) {
|
|
969
|
+
act.result = {
|
|
970
|
+
href: href || 'NONE',
|
|
971
|
+
target: target || 'NONE',
|
|
972
|
+
move: 'clicked'
|
|
973
|
+
};
|
|
974
|
+
}
|
|
973
975
|
}
|
|
974
976
|
// Otherwise, if it is selecting an option in a select list, perform it.
|
|
975
977
|
else if (act.type === 'select') {
|
|
976
|
-
const options = await
|
|
978
|
+
const options = await matchingElement.$$('option');
|
|
977
979
|
let optionText = '';
|
|
978
980
|
if (options && Array.isArray(options) && options.length) {
|
|
979
981
|
const optionTexts = [];
|
|
@@ -984,12 +986,12 @@ const doActs = async (report, actIndex, page) => {
|
|
|
984
986
|
const matchTexts = optionTexts.map((text, index) => text.includes(act.what) ? index : -1);
|
|
985
987
|
const index = matchTexts.filter(text => text > -1)[act.index || 0];
|
|
986
988
|
if (index !== undefined) {
|
|
987
|
-
await
|
|
989
|
+
await matchingElement.selectOption({index});
|
|
988
990
|
optionText = optionTexts[index];
|
|
989
991
|
}
|
|
990
992
|
}
|
|
991
993
|
act.result = optionText
|
|
992
|
-
?
|
|
994
|
+
? `“${optionText}” selected`
|
|
993
995
|
: 'ERROR: option not found';
|
|
994
996
|
}
|
|
995
997
|
// Otherwise, if it is entering text on the element:
|
|
@@ -1003,19 +1005,23 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1003
1005
|
what = what.replace(/__[A-Z]+__/, envValue);
|
|
1004
1006
|
}
|
|
1005
1007
|
// Enter the text.
|
|
1006
|
-
await
|
|
1008
|
+
await matchingElement.type(act.what);
|
|
1007
1009
|
report.presses += act.what.length;
|
|
1008
1010
|
act.result = 'entered';
|
|
1009
1011
|
}
|
|
1010
1012
|
// Otherwise, i.e. if the move is unknown, add the failure to the act.
|
|
1011
1013
|
else {
|
|
1012
1014
|
// Report the error.
|
|
1013
|
-
|
|
1015
|
+
const report = 'ERROR: move unknown';
|
|
1016
|
+
act.result = report;
|
|
1017
|
+
console.log(report);
|
|
1014
1018
|
}
|
|
1015
1019
|
}
|
|
1016
|
-
// Otherwise, i.e. if
|
|
1020
|
+
// Otherwise, i.e. if no match was found:
|
|
1017
1021
|
else {
|
|
1018
|
-
|
|
1022
|
+
const report = 'ERROR: Specified element not found';
|
|
1023
|
+
act.result = report;
|
|
1024
|
+
console.log(report);
|
|
1019
1025
|
}
|
|
1020
1026
|
}
|
|
1021
1027
|
// Otherwise, if the act is a keypress:
|
|
@@ -1203,7 +1209,9 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1203
1209
|
// Otherwise, i.e. if the command is invalid:
|
|
1204
1210
|
else {
|
|
1205
1211
|
// Add an error result to the act.
|
|
1206
|
-
|
|
1212
|
+
const errorMsg = `ERROR: Invalid command of type ${act.type}`;
|
|
1213
|
+
act.result = errorMsg;
|
|
1214
|
+
console.log(errorMsg);
|
|
1207
1215
|
}
|
|
1208
1216
|
// Perform the remaining acts.
|
|
1209
1217
|
await doActs(report, actIndex + 1, page);
|