testaro 5.9.2 → 5.11.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/README.md +2 -0
- package/commands.js +2 -2
- package/package.json +1 -1
- package/run.js +226 -178
- package/tests/elements.js +123 -103
package/README.md
CHANGED
|
@@ -316,6 +316,8 @@ Tenon recommends giving it a public URL rather than giving it the content of a p
|
|
|
316
316
|
|
|
317
317
|
The `continuum` test makes use of the files in the `continuum` directory. The test inserts the contents of all three files into the page as scripts and then uses them to perform the tests of the Continuum package.
|
|
318
318
|
|
|
319
|
+
Level Access on 22 August 2022 granted authorization for the copying of the `AccessEngine.community.js` file insofar as necessary for allowing Continuum community edition tests to be included in Testaro.
|
|
320
|
+
|
|
319
321
|
###### HTML CodeSniffer
|
|
320
322
|
|
|
321
323
|
The `htmlcs` test makes use of the`htmlcs/HTMLCS.js` file. That file was created, and can be recreated if necessary, as follows:
|
package/commands.js
CHANGED
|
@@ -32,7 +32,7 @@ exports.commands = {
|
|
|
32
32
|
}
|
|
33
33
|
],
|
|
34
34
|
link: [
|
|
35
|
-
'Click a link',
|
|
35
|
+
'Click a link and wait for the page to be idle or loaded',
|
|
36
36
|
{
|
|
37
37
|
which: [true, 'string', 'hasLength', 'substring of link text'],
|
|
38
38
|
index: [false, 'number', '', 'index among matches if not 0'],
|
|
@@ -153,7 +153,7 @@ exports.commands = {
|
|
|
153
153
|
elements: [
|
|
154
154
|
'Perform an elements test',
|
|
155
155
|
{
|
|
156
|
-
detailLevel: [true, 'number', '', '0
|
|
156
|
+
detailLevel: [true, 'number', '', '0 to 3, to specify the level of detail'],
|
|
157
157
|
tagName: [false, 'string', '', 'tag name of elements'],
|
|
158
158
|
onlyVisible: [false, 'boolean', '', 'whether to exclude invisible elements'],
|
|
159
159
|
attribute: [false, 'string', 'hasLength', 'required attribute or attribute=value']
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -12,7 +12,7 @@ const {commands} = require('./commands');
|
|
|
12
12
|
|
|
13
13
|
// ########## CONSTANTS
|
|
14
14
|
|
|
15
|
-
// Set DEBUG environment variable to 'true' to add debugging features.
|
|
15
|
+
// Set DEBUG environment variable to 'true,' to add debugging features.
|
|
16
16
|
const debug = process.env.DEBUG === 'true';
|
|
17
17
|
// Set WAITS environment variable to a positive number to insert delays (in ms).
|
|
18
18
|
const waits = Number.parseInt(process.env.WAITS) || 0;
|
|
@@ -294,16 +294,11 @@ const launch = async typeName => {
|
|
|
294
294
|
});
|
|
295
295
|
// If the launch succeeded:
|
|
296
296
|
if (healthy) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
} : {};
|
|
304
|
-
browserContext = await browser.newContext(viewport);
|
|
305
|
-
// When a page is added to the browser context:
|
|
306
|
-
browserContext.on('page', page => {
|
|
297
|
+
browserContext = await browser.newContext();
|
|
298
|
+
// When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
|
|
299
|
+
browserContext.on('page', async page => {
|
|
300
|
+
// Activate the page.
|
|
301
|
+
await page.bringToFront();
|
|
307
302
|
// Make abbreviations of its console messages get reported in the Playwright console.
|
|
308
303
|
page.on('console', msg => {
|
|
309
304
|
const msgText = msg.text();
|
|
@@ -339,12 +334,6 @@ const launch = async typeName => {
|
|
|
339
334
|
});
|
|
340
335
|
// Open the first page of the context.
|
|
341
336
|
const page = await browserContext.newPage();
|
|
342
|
-
if (debug) {
|
|
343
|
-
page.setViewportSize({
|
|
344
|
-
width: 1280,
|
|
345
|
-
height: 1120
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
337
|
// Wait until it is stable.
|
|
349
338
|
await page.waitForLoadState('domcontentloaded');
|
|
350
339
|
// Update the name of the current browser type and store it in the page.
|
|
@@ -445,85 +434,6 @@ const textOf = async (page, element) => {
|
|
|
445
434
|
return null;
|
|
446
435
|
}
|
|
447
436
|
};
|
|
448
|
-
// Returns an element of a type case-insensitively including a text.
|
|
449
|
-
const matchElement = async (page, selector, matchText, index = 0) => {
|
|
450
|
-
if (matchText) {
|
|
451
|
-
// If the page still exists:
|
|
452
|
-
if (page) {
|
|
453
|
-
const slimText = debloat(matchText);
|
|
454
|
-
// Identify the elements of the specified type.
|
|
455
|
-
const selections = await page.$$(selector);
|
|
456
|
-
// If there are any:
|
|
457
|
-
if (selections.length) {
|
|
458
|
-
// If there are enough to make a match possible:
|
|
459
|
-
if (index < selections.length) {
|
|
460
|
-
// For each element of the specified type:
|
|
461
|
-
let matchCount = 0;
|
|
462
|
-
const selectionTexts = [];
|
|
463
|
-
for (const selection of selections) {
|
|
464
|
-
// Add its text to the list of texts of such elements.
|
|
465
|
-
const selectionText = await textOf(page, selection);
|
|
466
|
-
selectionTexts.push(selectionText);
|
|
467
|
-
// If its text includes the specified text:
|
|
468
|
-
if (selectionText.includes(slimText)) {
|
|
469
|
-
// If the count of such elements with such texts found so far is the specified count:
|
|
470
|
-
if (matchCount++ === index) {
|
|
471
|
-
// Return it as the matching element.
|
|
472
|
-
return {
|
|
473
|
-
success: true,
|
|
474
|
-
matchingElement: selection
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
// None satisfied the specifications, so return a failure.
|
|
480
|
-
return {
|
|
481
|
-
success: false,
|
|
482
|
-
error: 'exhausted',
|
|
483
|
-
message: `Text found in only ${matchCount} (not ${index + 1}) of ${selections.length}`,
|
|
484
|
-
candidateTexts: selectionTexts
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
// Otherwise, i.e. if there are too few such elements to make a match possible:
|
|
488
|
-
else {
|
|
489
|
-
// Return a failure.
|
|
490
|
-
return {
|
|
491
|
-
success: false,
|
|
492
|
-
error: 'fewer',
|
|
493
|
-
message: `Count of '${selector}' elements only ${selections.length}`
|
|
494
|
-
};
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
// Otherwise, i.e. if there are no elements of the specified type:
|
|
498
|
-
else {
|
|
499
|
-
// Return a failure.
|
|
500
|
-
return {
|
|
501
|
-
success: false,
|
|
502
|
-
error: 'none',
|
|
503
|
-
message: `No '${selector}' elements found`
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
// Otherwise, i.e. if the page no longer exists:
|
|
508
|
-
else {
|
|
509
|
-
// Return a failure.
|
|
510
|
-
return {
|
|
511
|
-
success: false,
|
|
512
|
-
error: 'gone',
|
|
513
|
-
message: 'Page gone'
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
// Otherwise, i.e. if no text was specified:
|
|
518
|
-
else {
|
|
519
|
-
// Return a failure.
|
|
520
|
-
return {
|
|
521
|
-
success: false,
|
|
522
|
-
error: 'text',
|
|
523
|
-
message: 'No text specified'
|
|
524
|
-
};
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
437
|
// Returns a string with any final slash removed.
|
|
528
438
|
const deSlash = string => string.endsWith('/') ? string.slice(0, -1) : string;
|
|
529
439
|
// Tries to visit a URL.
|
|
@@ -682,10 +592,10 @@ const wait = ms => {
|
|
|
682
592
|
});
|
|
683
593
|
};
|
|
684
594
|
// Adds an error result to an act.
|
|
685
|
-
const addError = (act, error) => {
|
|
686
|
-
act.result =
|
|
687
|
-
|
|
688
|
-
|
|
595
|
+
const addError = (act, error, message) => {
|
|
596
|
+
act.result.success = false;
|
|
597
|
+
act.result.error = error;
|
|
598
|
+
act.result.message = message;
|
|
689
599
|
if (act.type === 'test') {
|
|
690
600
|
act.result.prevented = true;
|
|
691
601
|
}
|
|
@@ -715,8 +625,8 @@ const doActs = async (report, actIndex, page) => {
|
|
|
715
625
|
const logSuffix = condition.length === 3 ? ` ${condition[1]} ${condition[2]}` : '';
|
|
716
626
|
console.log(`>> ${condition[0]}${logSuffix}`);
|
|
717
627
|
// Identify the act to be checked.
|
|
718
|
-
const ifActIndex = report.acts.map(act => act.type !== 'next').lastIndexOf(true);
|
|
719
|
-
// Determine whether its jump condition is true
|
|
628
|
+
const ifActIndex = report.acts.map(act => act.type !== 'next').lastIndexOf(true,);
|
|
629
|
+
// Determine whether its jump condition is true,.
|
|
720
630
|
const truth = isTrue(report.acts[ifActIndex].result, condition);
|
|
721
631
|
// Add the result to the act.
|
|
722
632
|
act.result = {
|
|
@@ -726,7 +636,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
726
636
|
value: truth[0],
|
|
727
637
|
jumpRequired: truth[1]
|
|
728
638
|
};
|
|
729
|
-
// If the condition is true
|
|
639
|
+
// If the condition is true,:
|
|
730
640
|
if (truth[1]) {
|
|
731
641
|
// If the performance of commands is to stop:
|
|
732
642
|
if (act.jump === 0) {
|
|
@@ -764,8 +674,9 @@ const doActs = async (report, actIndex, page) => {
|
|
|
764
674
|
const {what, which} = act;
|
|
765
675
|
console.log(`>> ${what}`);
|
|
766
676
|
const result = act.result = {};
|
|
767
|
-
//
|
|
677
|
+
// If the text is to be the URL:
|
|
768
678
|
if (what === 'url') {
|
|
679
|
+
// Wait for it up to 15 seconds and quit on failure.
|
|
769
680
|
try {
|
|
770
681
|
await page.waitForURL(which, {timeout: 15000});
|
|
771
682
|
result.found = true;
|
|
@@ -776,7 +687,9 @@ const doActs = async (report, actIndex, page) => {
|
|
|
776
687
|
waitError(page, act, error, 'URL');
|
|
777
688
|
}
|
|
778
689
|
}
|
|
690
|
+
// Otherwise, if the text is to be a substring of the page title:
|
|
779
691
|
else if (what === 'title') {
|
|
692
|
+
// Wait for it up to 5 seconds and quit on failure.
|
|
780
693
|
try {
|
|
781
694
|
await page.waitForFunction(
|
|
782
695
|
text => document
|
|
@@ -789,14 +702,16 @@ const doActs = async (report, actIndex, page) => {
|
|
|
789
702
|
}
|
|
790
703
|
);
|
|
791
704
|
result.found = true;
|
|
792
|
-
result.title = await page.title();
|
|
705
|
+
result.title = await page.title();
|
|
793
706
|
}
|
|
794
707
|
catch(error) {
|
|
795
708
|
actIndex = -2;
|
|
796
709
|
waitError(page, act, error, 'title');
|
|
797
710
|
}
|
|
798
711
|
}
|
|
712
|
+
// Otherwise, if the text is to be a substring of the text of the page body:
|
|
799
713
|
else if (what === 'body') {
|
|
714
|
+
// Wait for it up to 10 seconds and quit on failure.
|
|
800
715
|
try {
|
|
801
716
|
await page.waitForFunction(
|
|
802
717
|
text => document
|
|
@@ -818,27 +733,33 @@ const doActs = async (report, actIndex, page) => {
|
|
|
818
733
|
}
|
|
819
734
|
// Otherwise, if the act is a wait for a state:
|
|
820
735
|
else if (act.type === 'state') {
|
|
821
|
-
// Wait for it, and quit
|
|
736
|
+
// Wait for it up to 5 or 10 seconrds, and quit on failure.
|
|
822
737
|
const stateIndex = ['loaded', 'idle'].indexOf(act.which);
|
|
823
738
|
await page.waitForLoadState(
|
|
824
739
|
['domcontentloaded', 'networkidle'][stateIndex], {timeout: [10000, 5000][stateIndex]}
|
|
825
740
|
)
|
|
826
741
|
.catch(error => {
|
|
827
742
|
console.log(`ERROR waiting for page to be ${act.which} (${error.message})`);
|
|
828
|
-
|
|
743
|
+
act.result = {
|
|
744
|
+
success: false,
|
|
745
|
+
error: `ERROR waiting for page to be ${act.which}`
|
|
746
|
+
};
|
|
829
747
|
actIndex = -2;
|
|
830
748
|
});
|
|
831
749
|
if (actIndex > -2) {
|
|
832
|
-
|
|
750
|
+
act.result = {
|
|
751
|
+
success: true,
|
|
752
|
+
state: act.which
|
|
753
|
+
};
|
|
833
754
|
}
|
|
834
755
|
}
|
|
835
756
|
// Otherwise, if the act is a page switch:
|
|
836
757
|
else if (act.type === 'page') {
|
|
837
758
|
// Wait for a page to be created and identify it as current.
|
|
838
759
|
page = await browserContext.waitForEvent('page');
|
|
839
|
-
// Wait
|
|
760
|
+
// Wait up to 20 seconds until it is idle.
|
|
840
761
|
await page.waitForLoadState('networkidle', {timeout: 20000});
|
|
841
|
-
// Add the resulting URL
|
|
762
|
+
// Add the resulting URL to the act.
|
|
842
763
|
const result = {
|
|
843
764
|
url: page.url()
|
|
844
765
|
};
|
|
@@ -855,7 +776,9 @@ const doActs = async (report, actIndex, page) => {
|
|
|
855
776
|
if (act.type === 'reveal') {
|
|
856
777
|
// Make all elements in the page visible.
|
|
857
778
|
await require('./procs/allVis').allVis(page);
|
|
858
|
-
act.result =
|
|
779
|
+
act.result = {
|
|
780
|
+
success: true,
|
|
781
|
+
};
|
|
859
782
|
}
|
|
860
783
|
// Otherwise, if the act is a tenon request:
|
|
861
784
|
else if (act.type === 'tenonRequest') {
|
|
@@ -1010,106 +933,211 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1010
933
|
});
|
|
1011
934
|
testReport.result.failureCount = failureCount;
|
|
1012
935
|
}
|
|
936
|
+
testReport.result.success = true;
|
|
1013
937
|
report.testTimes.push([act.which, Math.round((Date.now() - startTime) / 1000)]);
|
|
1014
938
|
report.testTimes.sort((a, b) => b[1] - a[1]);
|
|
1015
939
|
// Add the result object (possibly an array) to the act.
|
|
1016
940
|
const resultCount = Object.keys(testReport.result).length;
|
|
1017
|
-
act.result = resultCount ? testReport.result :
|
|
941
|
+
act.result = resultCount ? testReport.result : {success: false};
|
|
1018
942
|
}
|
|
1019
943
|
// Otherwise, if the act is a move:
|
|
1020
944
|
else if (moves[act.type]) {
|
|
1021
945
|
const selector = typeof moves[act.type] === 'string' ? moves[act.type] : act.what;
|
|
1022
|
-
// Try up to
|
|
1023
|
-
|
|
946
|
+
// Try for up to 10 seconds to identify the element to perform the move on.
|
|
947
|
+
act.result = {found: false};
|
|
948
|
+
let selection = {};
|
|
1024
949
|
let tries = 0;
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
if (
|
|
950
|
+
const slimText = debloat(act.which);
|
|
951
|
+
while (tries++ < 5 && ! act.result.found) {
|
|
952
|
+
if (act.which) {
|
|
953
|
+
// If the page still exists:
|
|
954
|
+
if (page) {
|
|
955
|
+
// Identify the elements of the specified type.
|
|
956
|
+
const selections = await page.$$(selector);
|
|
957
|
+
// If there are any:
|
|
958
|
+
if (selections.length) {
|
|
959
|
+
// If there are enough to make a match possible:
|
|
960
|
+
if ((act.index || 0) < selections.length) {
|
|
961
|
+
// For each element of the specified type:
|
|
962
|
+
let matchCount = 0;
|
|
963
|
+
const selectionTexts = [];
|
|
964
|
+
for (selection of selections) {
|
|
965
|
+
// Add its text to the list of texts of such elements.
|
|
966
|
+
const selectionText = await textOf(page, selection);
|
|
967
|
+
selectionTexts.push(selectionText);
|
|
968
|
+
// If its text includes the specified text:
|
|
969
|
+
if (selectionText.includes(slimText)) {
|
|
970
|
+
// If the element has the specified index among such elements:
|
|
971
|
+
if (matchCount++ === (act.index || 0)) {
|
|
972
|
+
// Report it as the matching element and stop checking.
|
|
973
|
+
act.result.found = true;
|
|
974
|
+
act.result.text = slimText;
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
// If no element satisfied the specifications:
|
|
980
|
+
if (! act.result.found) {
|
|
981
|
+
act.result.success = false;
|
|
982
|
+
act.result.error = 'exhausted';
|
|
983
|
+
act.result.typeElementCount = selections.length;
|
|
984
|
+
act.result.textElementCount = --matchCount;
|
|
985
|
+
act.result.message = 'Not enough elements have the specified text';
|
|
986
|
+
act.result.candidateTexts = selectionTexts;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// Otherwise, i.e. if there are too few such elements to make a match possible:
|
|
990
|
+
else {
|
|
991
|
+
// Return a failure.
|
|
992
|
+
act.result.success = false;
|
|
993
|
+
act.result.error = 'fewer';
|
|
994
|
+
act.result.typeElementCount = selections.length;
|
|
995
|
+
act.result.message = 'Elements of specified type too few';
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
// Otherwise, i.e. if there are no elements of the specified type:
|
|
999
|
+
else {
|
|
1000
|
+
// Return a failure.
|
|
1001
|
+
act.result.success = false;
|
|
1002
|
+
act.result.error = 'none';
|
|
1003
|
+
act.result.typeElementCount = 0;
|
|
1004
|
+
act.result.message = 'No elements specified type found';
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
// Otherwise, i.e. if the page no longer exists:
|
|
1008
|
+
else {
|
|
1009
|
+
// Return a failure.
|
|
1010
|
+
act.result.success = false;
|
|
1011
|
+
act.result.error = 'gone';
|
|
1012
|
+
act.result.message = 'Page gone';
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
// Otherwise, i.e. if no text was specified:
|
|
1016
|
+
else {
|
|
1017
|
+
// Return a failure.
|
|
1018
|
+
act.result.success = false;
|
|
1019
|
+
act.result.error = 'text';
|
|
1020
|
+
act.result.message = 'No text specified';
|
|
1021
|
+
}
|
|
1022
|
+
if (! act.result.found) {
|
|
1028
1023
|
await wait(2000);
|
|
1029
1024
|
}
|
|
1030
1025
|
}
|
|
1031
1026
|
// If a match was found:
|
|
1032
|
-
if (
|
|
1033
|
-
const {matchingElement} = matchResult;
|
|
1027
|
+
if (act.result.found) {
|
|
1034
1028
|
// If the move is a button click, perform it.
|
|
1035
1029
|
if (act.type === 'button') {
|
|
1036
|
-
await
|
|
1037
|
-
act.result =
|
|
1030
|
+
await selection.click({timeout: 3000});
|
|
1031
|
+
act.result.success = true;
|
|
1032
|
+
act.result.move = 'clicked';
|
|
1038
1033
|
}
|
|
1039
1034
|
// Otherwise, if it is checking a radio button or checkbox, perform it.
|
|
1040
1035
|
else if (['checkbox', 'radio'].includes(act.type)) {
|
|
1041
|
-
await
|
|
1036
|
+
await selection.waitForElementState('stable', {timeout: 2000})
|
|
1042
1037
|
.catch(error => {
|
|
1043
1038
|
console.log(`ERROR waiting for stable ${act.type} (${error.message})`);
|
|
1044
|
-
|
|
1039
|
+
act.result.success = false;
|
|
1040
|
+
act.result.error = `ERROR waiting for stable ${act.type}`;
|
|
1045
1041
|
});
|
|
1046
|
-
if (! act.result) {
|
|
1047
|
-
const isEnabled = await
|
|
1042
|
+
if (! act.result.error) {
|
|
1043
|
+
const isEnabled = await selection.isEnabled();
|
|
1048
1044
|
if (isEnabled) {
|
|
1049
|
-
await
|
|
1045
|
+
await selection.check({
|
|
1050
1046
|
force: true,
|
|
1051
1047
|
timeout: 2000
|
|
1052
1048
|
})
|
|
1053
1049
|
.catch(error => {
|
|
1054
1050
|
console.log(`ERROR checking ${act.type} (${error.message})`);
|
|
1055
|
-
|
|
1051
|
+
act.result.success = false;
|
|
1052
|
+
act.result.error = `ERROR checking ${act.type}`;
|
|
1056
1053
|
});
|
|
1057
|
-
if (! act.result) {
|
|
1058
|
-
act.result =
|
|
1054
|
+
if (! act.result.error) {
|
|
1055
|
+
act.result.success = true;
|
|
1056
|
+
act.result.move = 'checked';
|
|
1059
1057
|
}
|
|
1060
1058
|
}
|
|
1061
1059
|
else {
|
|
1062
1060
|
const report = `ERROR: could not check ${act.type} because disabled`;
|
|
1063
1061
|
console.log(report);
|
|
1064
|
-
act.result =
|
|
1062
|
+
act.result.success = false;
|
|
1063
|
+
act.result.error = report;
|
|
1065
1064
|
}
|
|
1066
1065
|
}
|
|
1067
1066
|
}
|
|
1068
1067
|
// Otherwise, if it is focusing the element, perform it.
|
|
1069
1068
|
else if (act.type === 'focus') {
|
|
1070
|
-
await
|
|
1071
|
-
act.result =
|
|
1069
|
+
await selection.focus({timeout: 2000});
|
|
1070
|
+
act.result.success = true;
|
|
1071
|
+
act.result.move = 'focused';
|
|
1072
1072
|
}
|
|
1073
1073
|
// Otherwise, if it is clicking a link:
|
|
1074
1074
|
else if (act.type === 'link') {
|
|
1075
|
-
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
// If
|
|
1080
|
-
|
|
1081
|
-
//
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1075
|
+
const href = await selection.getAttribute('href');
|
|
1076
|
+
const target = await selection.getAttribute('target');
|
|
1077
|
+
act.result.href = href || 'NONE';
|
|
1078
|
+
act.result.target = target || 'DEFAULT';
|
|
1079
|
+
// If the destination is a new page:
|
|
1080
|
+
if (target && target !== '_self') {
|
|
1081
|
+
// Click the link and wait for the resulting page event.
|
|
1082
|
+
try {
|
|
1083
|
+
const [newPage] = await Promise.all([
|
|
1084
|
+
page.context().waitForEvent('page', {timeout: 6000}),
|
|
1085
|
+
selection.click({timeout: 5000})
|
|
1086
|
+
]);
|
|
1087
|
+
// Wait for the new page to load.
|
|
1088
|
+
await newPage.waitForLoadState('domcontentloaded', {timeout: 6000});
|
|
1089
|
+
// Make the new page the current page.
|
|
1090
|
+
page = newPage;
|
|
1091
|
+
act.result.success = true;
|
|
1092
|
+
act.result.move = 'clicked';
|
|
1093
|
+
act.result.newURL = page.url();
|
|
1094
|
+
}
|
|
1095
|
+
// If the click, event, or load failed:
|
|
1096
|
+
catch(error) {
|
|
1089
1097
|
// Quit and report the failure.
|
|
1098
|
+
console.log(
|
|
1099
|
+
`ERROR clicking new-page link (${error.message.replace(/\n.+/s, '')})`
|
|
1100
|
+
);
|
|
1101
|
+
act.result.success = false;
|
|
1102
|
+
act.result.error = 'unclickable';
|
|
1103
|
+
act.result.message = 'ERROR: click and new-page navigation timed out';
|
|
1090
1104
|
actIndex = -2;
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
// Otherwise, i.e. if the destination is in the current page:
|
|
1108
|
+
else {
|
|
1109
|
+
// Click the link and wait for the resulting navigation.
|
|
1110
|
+
await selection.click({timeout: 5000})
|
|
1111
|
+
// If the click and navigation time out:
|
|
1112
|
+
.catch(async error => {
|
|
1113
|
+
// Try to force-click it and wait for the navigation.
|
|
1114
|
+
const errorSummary = error.message.replace(/\n.+/s, '');
|
|
1115
|
+
console.log(`ERROR: Link to ${href} not clickable (${errorSummary})`);
|
|
1116
|
+
await selection.click({
|
|
1117
|
+
force: true,
|
|
1118
|
+
timeout: 3000
|
|
1119
|
+
})
|
|
1120
|
+
// If it cannot be force-clicked:
|
|
1121
|
+
.catch(error => {
|
|
1122
|
+
// Quit and report the failure.
|
|
1123
|
+
actIndex = -2;
|
|
1124
|
+
const errorSummary = error.message.replace(/\n.+/s, '');
|
|
1125
|
+
console.log(`ERROR: Link to ${href} not force-clickable (${errorSummary})`);
|
|
1126
|
+
act.result.success = false;
|
|
1127
|
+
act.result.error = 'unclickable';
|
|
1128
|
+
act.result.message = 'ERROR: Normal and forced click attempts timed out';
|
|
1129
|
+
});
|
|
1098
1130
|
});
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
href: href || 'NONE',
|
|
1105
|
-
target: target || 'NONE',
|
|
1106
|
-
move: 'clicked'
|
|
1107
|
-
};
|
|
1131
|
+
// If the link click succeeded:
|
|
1132
|
+
if (! act.result.error) {
|
|
1133
|
+
act.result.success = true;
|
|
1134
|
+
act.result.move = 'clicked';
|
|
1135
|
+
}
|
|
1108
1136
|
}
|
|
1109
1137
|
}
|
|
1110
1138
|
// Otherwise, if it is selecting an option in a select list, perform it.
|
|
1111
1139
|
else if (act.type === 'select') {
|
|
1112
|
-
const options = await
|
|
1140
|
+
const options = await selection.$$('option');
|
|
1113
1141
|
let optionText = '';
|
|
1114
1142
|
if (options && Array.isArray(options) && options.length) {
|
|
1115
1143
|
const optionTexts = [];
|
|
@@ -1122,13 +1150,13 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1122
1150
|
);
|
|
1123
1151
|
const index = matchTexts.filter(text => text > -1)[act.index || 0];
|
|
1124
1152
|
if (index !== undefined) {
|
|
1125
|
-
await
|
|
1153
|
+
await selection.selectOption({index});
|
|
1126
1154
|
optionText = optionTexts[index];
|
|
1127
1155
|
}
|
|
1128
1156
|
}
|
|
1129
|
-
act.result =
|
|
1130
|
-
|
|
1131
|
-
|
|
1157
|
+
act.result.success = true;
|
|
1158
|
+
act.result.move = 'selected';
|
|
1159
|
+
act.result.option = optionText;
|
|
1132
1160
|
}
|
|
1133
1161
|
// Otherwise, if it is entering text on the element:
|
|
1134
1162
|
else if (act.type === 'text') {
|
|
@@ -1141,22 +1169,26 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1141
1169
|
what = what.replace(/__[A-Z]+__/, envValue);
|
|
1142
1170
|
}
|
|
1143
1171
|
// Enter the text.
|
|
1144
|
-
await
|
|
1172
|
+
await selection.type(act.what);
|
|
1145
1173
|
report.presses += act.what.length;
|
|
1146
|
-
act.result =
|
|
1174
|
+
act.result.success = true;
|
|
1175
|
+
act.result.move = 'entered';
|
|
1147
1176
|
}
|
|
1148
1177
|
// Otherwise, i.e. if the move is unknown, add the failure to the act.
|
|
1149
1178
|
else {
|
|
1150
1179
|
// Report the error.
|
|
1151
1180
|
const report = 'ERROR: move unknown';
|
|
1152
|
-
act.result =
|
|
1181
|
+
act.result.success = false;
|
|
1182
|
+
act.result.error = report;
|
|
1153
1183
|
console.log(report);
|
|
1154
1184
|
}
|
|
1155
1185
|
}
|
|
1156
1186
|
// Otherwise, i.e. if no match was found:
|
|
1157
1187
|
else {
|
|
1158
1188
|
// Stop.
|
|
1159
|
-
act.result =
|
|
1189
|
+
act.result.success = false;
|
|
1190
|
+
act.result.error = 'absent';
|
|
1191
|
+
act.result.message = 'ERROR: specified element not found';
|
|
1160
1192
|
console.log('ERROR: Specified element not found');
|
|
1161
1193
|
actIndex = -2;
|
|
1162
1194
|
}
|
|
@@ -1172,7 +1204,10 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1172
1204
|
await page.keyboard.press(key);
|
|
1173
1205
|
}
|
|
1174
1206
|
const qualifier = act.again ? `${1 + act.again} times` : 'once';
|
|
1175
|
-
act.result =
|
|
1207
|
+
act.result = {
|
|
1208
|
+
success: true,
|
|
1209
|
+
message: `pressed ${qualifier}`
|
|
1210
|
+
};
|
|
1176
1211
|
}
|
|
1177
1212
|
// Otherwise, if it is a repetitive keyboard navigation:
|
|
1178
1213
|
else if (act.type === 'presses') {
|
|
@@ -1303,6 +1338,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1303
1338
|
}
|
|
1304
1339
|
// Add the result to the act.
|
|
1305
1340
|
act.result = {
|
|
1341
|
+
success: true,
|
|
1306
1342
|
status,
|
|
1307
1343
|
totals: {
|
|
1308
1344
|
presses,
|
|
@@ -1322,32 +1358,44 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1322
1358
|
// Otherwise, i.e. if the act type is unknown:
|
|
1323
1359
|
else {
|
|
1324
1360
|
// Add the error result to the act.
|
|
1325
|
-
act.result =
|
|
1361
|
+
act.result = {
|
|
1362
|
+
success: false,
|
|
1363
|
+
error: 'badType',
|
|
1364
|
+
message: 'ERROR: invalid command type'
|
|
1365
|
+
};
|
|
1326
1366
|
}
|
|
1327
1367
|
}
|
|
1328
1368
|
// Otherwise, i.e. if redirection is prohibited but occurred:
|
|
1329
1369
|
else {
|
|
1330
|
-
// Add
|
|
1331
|
-
|
|
1370
|
+
// Add an error result to the act.
|
|
1371
|
+
act.result = {
|
|
1372
|
+
success: false,
|
|
1373
|
+
error: 'redirection',
|
|
1374
|
+
message: `ERROR: Page redirected to (${url})`
|
|
1375
|
+
};
|
|
1332
1376
|
}
|
|
1333
1377
|
}
|
|
1334
1378
|
// Otherwise, i.e. if the required page URL does not exist:
|
|
1335
1379
|
else {
|
|
1336
1380
|
// Add an error result to the act.
|
|
1337
|
-
addError(act, 'ERROR: Page has no URL');
|
|
1381
|
+
addError(act, 'noURL', 'ERROR: Page has no URL');
|
|
1338
1382
|
}
|
|
1339
1383
|
}
|
|
1340
1384
|
// Otherwise, i.e. if no page exists:
|
|
1341
1385
|
else {
|
|
1342
1386
|
// Add an error result to the act.
|
|
1343
|
-
addError(act, 'ERROR: No page identified');
|
|
1387
|
+
addError(act, 'noPage', 'ERROR: No page identified');
|
|
1344
1388
|
}
|
|
1345
1389
|
}
|
|
1346
1390
|
// Otherwise, i.e. if the command is invalid:
|
|
1347
1391
|
else {
|
|
1348
1392
|
// Add an error result to the act.
|
|
1349
1393
|
const errorMsg = `ERROR: Invalid command of type ${act.type}`;
|
|
1350
|
-
act.result =
|
|
1394
|
+
act.result = {
|
|
1395
|
+
success: false,
|
|
1396
|
+
error: 'badCommand',
|
|
1397
|
+
message: errorMsg
|
|
1398
|
+
};
|
|
1351
1399
|
console.log(errorMsg);
|
|
1352
1400
|
// Quit.
|
|
1353
1401
|
actIndex = -2;
|
package/tests/elements.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
/*
|
|
2
2
|
elements
|
|
3
3
|
This test reports data about specified elements.
|
|
4
|
+
Meanings of detailLevel values:
|
|
5
|
+
0. Only total element count; no detail.
|
|
6
|
+
1. Also data on each specified element.
|
|
7
|
+
2. Data on each specified element also include the text content of the parent element.
|
|
8
|
+
3. Data on each specified element also include data on its sibling nodes.
|
|
4
9
|
*/
|
|
5
10
|
exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) => {
|
|
6
11
|
// Determine a selector of the specified elements.
|
|
@@ -11,120 +16,135 @@ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) =>
|
|
|
11
16
|
if (onlyVisible) {
|
|
12
17
|
selector += ':visible';
|
|
13
18
|
}
|
|
19
|
+
let data = {};
|
|
14
20
|
// Get the data on the elements.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (nodeType === 1) {
|
|
25
|
-
sibInfo.tagName = node.tagName;
|
|
26
|
-
}
|
|
27
|
-
else if (nodeType === 3) {
|
|
28
|
-
sibInfo.text = compact(text);
|
|
29
|
-
}
|
|
30
|
-
return sibInfo;
|
|
31
|
-
};
|
|
32
|
-
// FUNCTION DEFINITIONS END
|
|
33
|
-
// Initialize the data with the count of the specified elements.
|
|
34
|
-
const data = {
|
|
35
|
-
total: elements.length
|
|
36
|
-
};
|
|
37
|
-
// If no itemization is required:
|
|
38
|
-
if (detailLevel === 0) {
|
|
39
|
-
// Return the element count.
|
|
40
|
-
return data;
|
|
41
|
-
}
|
|
42
|
-
// Otherwise, i.e. if itemization is required:
|
|
43
|
-
else {
|
|
44
|
-
// Initialize the item data.
|
|
45
|
-
data.items = [];
|
|
46
|
-
// For each specified element:
|
|
47
|
-
elements.forEach(element => {
|
|
48
|
-
// Initialize data on it.
|
|
49
|
-
const parent = element.parentElement;
|
|
50
|
-
const datum = {
|
|
51
|
-
tagName: element.tagName,
|
|
52
|
-
parentTagName: parent ? parent.tagName : '',
|
|
53
|
-
code: compact(element.outerHTML),
|
|
54
|
-
attributes: [],
|
|
55
|
-
textContent: compact(element.textContent)
|
|
21
|
+
try {
|
|
22
|
+
data = await page.$$eval(selector, (elements, detailLevel) => {
|
|
23
|
+
// FUNCTION DEFINITIONS START
|
|
24
|
+
// Compacts a string.
|
|
25
|
+
const compact = string => string.replace(/\s+/g, ' ').trim();
|
|
26
|
+
// Gets data on the sibling nodes of an element.
|
|
27
|
+
const getSibInfo = (node, nodeType, text) => {
|
|
28
|
+
const sibInfo = {
|
|
29
|
+
type: nodeType
|
|
56
30
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// Add data on the attribute to the element data.
|
|
60
|
-
const {name, value} = attribute;
|
|
61
|
-
datum.attributes.push({
|
|
62
|
-
name,
|
|
63
|
-
value
|
|
64
|
-
});
|
|
65
|
-
// If the element has reference labels:
|
|
66
|
-
if (name === 'aria-labelledby') {
|
|
67
|
-
// Add their texts to the element data.
|
|
68
|
-
const labelerIDs = value.split(/\s+/);
|
|
69
|
-
const labelers = [];
|
|
70
|
-
labelerIDs.forEach(id => {
|
|
71
|
-
const labeler = document.getElementById(id);
|
|
72
|
-
if (labeler) {
|
|
73
|
-
labelers.push(compact(labeler.textContent));
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
if (labelers.length) {
|
|
77
|
-
datum.labelers = labelers;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
31
|
+
if (nodeType === 1) {
|
|
32
|
+
sibInfo.tagName = node.tagName;
|
|
80
33
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const compactContent = compact(textContent);
|
|
84
|
-
if (compactContent) {
|
|
85
|
-
// Add it to the element data.
|
|
86
|
-
datum.textContent = compactContent;
|
|
34
|
+
else if (nodeType === 3) {
|
|
35
|
+
sibInfo.text = compact(text);
|
|
87
36
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
37
|
+
return sibInfo;
|
|
38
|
+
};
|
|
39
|
+
// FUNCTION DEFINITIONS END
|
|
40
|
+
// Initialize the data with the count of the specified elements.
|
|
41
|
+
const data = {
|
|
42
|
+
total: elements.length
|
|
43
|
+
};
|
|
44
|
+
// If no itemization is required:
|
|
45
|
+
if (detailLevel === 0) {
|
|
46
|
+
// Return the element count.
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
// Otherwise, i.e. if itemization is required:
|
|
50
|
+
else {
|
|
51
|
+
// Initialize the item data.
|
|
52
|
+
data.items = [];
|
|
53
|
+
// For each specified element:
|
|
54
|
+
elements.forEach(element => {
|
|
55
|
+
// Initialize data on it.
|
|
56
|
+
const parent = element.parentElement;
|
|
57
|
+
const datum = {
|
|
58
|
+
tagName: element.tagName,
|
|
59
|
+
parentTagName: parent ? parent.tagName : '',
|
|
60
|
+
code: compact(element.outerHTML),
|
|
61
|
+
attributes: [],
|
|
62
|
+
textContent: compact(element.textContent)
|
|
99
63
|
};
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
64
|
+
// For each of its attributes:
|
|
65
|
+
for (const attribute of element.attributes) {
|
|
66
|
+
// Add data on the attribute to the element data.
|
|
67
|
+
const {name, value} = attribute;
|
|
68
|
+
datum.attributes.push({
|
|
69
|
+
name,
|
|
70
|
+
value
|
|
71
|
+
});
|
|
72
|
+
// If the element has reference labels:
|
|
73
|
+
if (name === 'aria-labelledby') {
|
|
74
|
+
// Add their texts to the element data.
|
|
75
|
+
const labelerIDs = value.split(/\s+/);
|
|
76
|
+
const labelers = [];
|
|
77
|
+
labelerIDs.forEach(id => {
|
|
78
|
+
const labeler = document.getElementById(id);
|
|
79
|
+
if (labeler) {
|
|
80
|
+
labelers.push(compact(labeler.textContent));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (labelers.length) {
|
|
84
|
+
datum.labelers = labelers;
|
|
108
85
|
}
|
|
109
86
|
}
|
|
110
87
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
88
|
+
// If the element has text content:
|
|
89
|
+
const {labels, textContent} = element;
|
|
90
|
+
const compactContent = compact(textContent);
|
|
91
|
+
if (compactContent) {
|
|
92
|
+
// Add it to the element data.
|
|
93
|
+
datum.textContent = compactContent;
|
|
94
|
+
}
|
|
95
|
+
// If the element has labels:
|
|
96
|
+
if (labels && labels.length) {
|
|
97
|
+
// Add their texts to the element data.
|
|
98
|
+
datum.labels = Array.from(labels).map(label => compact(label.textContent));
|
|
99
|
+
}
|
|
100
|
+
// If the parental text content is required:
|
|
101
|
+
if (detailLevel > 1) {
|
|
102
|
+
// Add it to the element data.
|
|
103
|
+
datum.parentTextContent = parent ? parent.textContent : '';
|
|
104
|
+
}
|
|
105
|
+
// If sibling itemization is required:
|
|
106
|
+
if (detailLevel === 3) {
|
|
107
|
+
// Add the sibling data to the element data.
|
|
108
|
+
datum.siblings = {
|
|
109
|
+
before: [],
|
|
110
|
+
after: []
|
|
111
|
+
};
|
|
112
|
+
let more = element;
|
|
113
|
+
while (more) {
|
|
114
|
+
more = more.previousSibling;
|
|
115
|
+
if (more) {
|
|
116
|
+
const {nodeType, nodeValue} = more;
|
|
117
|
+
if (! (nodeType === 3 && nodeValue === '')) {
|
|
118
|
+
const sibInfo = getSibInfo(more, nodeType, nodeValue);
|
|
119
|
+
datum.siblings.before.unshift(sibInfo);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
more = element;
|
|
124
|
+
while (more) {
|
|
125
|
+
more = more.nextSibling;
|
|
126
|
+
if (more) {
|
|
127
|
+
const {nodeType, textContent} = more;
|
|
128
|
+
if (! (nodeType === 3 && textContent === '')) {
|
|
129
|
+
const sibInfo = getSibInfo(more, nodeType, compact(textContent));
|
|
130
|
+
datum.siblings.after.push(sibInfo);
|
|
131
|
+
}
|
|
119
132
|
}
|
|
120
133
|
}
|
|
121
134
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
}
|
|
135
|
+
data.items.push(datum);
|
|
136
|
+
});
|
|
137
|
+
return data;
|
|
138
|
+
}
|
|
139
|
+
}, detailLevel);
|
|
140
|
+
}
|
|
141
|
+
catch(error) {
|
|
142
|
+
console.log(`ERROR performing test (${error.message})`);
|
|
143
|
+
data = {
|
|
144
|
+
prevented: true,
|
|
145
|
+
error: 'ERROR performing test'
|
|
146
|
+
};
|
|
147
|
+
}
|
|
128
148
|
// Return the result.
|
|
129
149
|
return {result: data};
|
|
130
150
|
};
|