testaro 5.12.1 → 5.13.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/package.json +1 -1
- package/run.js +92 -89
- package/tests/textNodes.js +14 -7
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -24,9 +24,9 @@ const moves = {
|
|
|
24
24
|
focus: true,
|
|
25
25
|
link: 'a, [role=link]',
|
|
26
26
|
radio: 'input[type=radio]',
|
|
27
|
-
search: 'input[type=search]',
|
|
27
|
+
search: 'input[type=search], input[aria-label*=search i], input[placeholder*=search i]',
|
|
28
28
|
select: 'select',
|
|
29
|
-
text: 'input[type=text]'
|
|
29
|
+
text: 'input[type=text], input:not([type])'
|
|
30
30
|
};
|
|
31
31
|
// Names and descriptions of tests.
|
|
32
32
|
const tests = {
|
|
@@ -114,6 +114,7 @@ let actCount = 0;
|
|
|
114
114
|
let browser;
|
|
115
115
|
let browserContext;
|
|
116
116
|
let browserTypeName;
|
|
117
|
+
let currentPage;
|
|
117
118
|
let requestedURL = '';
|
|
118
119
|
|
|
119
120
|
// ########## VALIDATORS
|
|
@@ -273,6 +274,8 @@ const browserClose = async () => {
|
|
|
273
274
|
await browser.close();
|
|
274
275
|
}
|
|
275
276
|
};
|
|
277
|
+
// Returns the first line of an error message.
|
|
278
|
+
const errorStart = error => error.message.replace(/\n.+/s, '');
|
|
276
279
|
// Launches a browser.
|
|
277
280
|
const launch = async typeName => {
|
|
278
281
|
const browserType = require('playwright')[typeName];
|
|
@@ -292,15 +295,15 @@ const launch = async typeName => {
|
|
|
292
295
|
browser = await browserType.launch(browserOptions)
|
|
293
296
|
.catch(error => {
|
|
294
297
|
healthy = false;
|
|
295
|
-
console.log(`ERROR launching browser
|
|
298
|
+
console.log(`ERROR launching browser (${errorStart(error)})`);
|
|
296
299
|
});
|
|
297
300
|
// If the launch succeeded:
|
|
298
301
|
if (healthy) {
|
|
299
302
|
browserContext = await browser.newContext();
|
|
300
303
|
// When a page (i.e. browser tab) is added to the browser context (i.e. browser window):
|
|
301
304
|
browserContext.on('page', async page => {
|
|
302
|
-
//
|
|
303
|
-
|
|
305
|
+
// Make the page current.
|
|
306
|
+
currentPage = page;
|
|
304
307
|
// Make abbreviations of its console messages get reported in the Playwright console.
|
|
305
308
|
page.on('console', msg => {
|
|
306
309
|
const msgText = msg.text();
|
|
@@ -335,11 +338,11 @@ const launch = async typeName => {
|
|
|
335
338
|
});
|
|
336
339
|
});
|
|
337
340
|
// Open the first page of the context.
|
|
338
|
-
|
|
341
|
+
currentPage = await browserContext.newPage();
|
|
339
342
|
// Wait until it is stable.
|
|
340
|
-
await
|
|
343
|
+
await currentPage.waitForLoadState('domcontentloaded', {timeout: 15000});
|
|
341
344
|
// Update the name of the current browser type and store it in the page.
|
|
342
|
-
|
|
345
|
+
currentPage.browserTypeName = browserTypeName = typeName;
|
|
343
346
|
}
|
|
344
347
|
}
|
|
345
348
|
};
|
|
@@ -450,7 +453,7 @@ const goto = async (page, url, timeout, waitUntil, isStrict) => {
|
|
|
450
453
|
waitUntil
|
|
451
454
|
})
|
|
452
455
|
.catch(error => {
|
|
453
|
-
console.log(`ERROR: Visit to ${url} timed out before ${waitUntil} (${error
|
|
456
|
+
console.log(`ERROR: Visit to ${url} timed out before ${waitUntil} (${errorStart(error)})`);
|
|
454
457
|
visitTimeoutCount++;
|
|
455
458
|
return 'error';
|
|
456
459
|
});
|
|
@@ -687,7 +690,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
687
690
|
}
|
|
688
691
|
catch(error) {
|
|
689
692
|
actIndex = -2;
|
|
690
|
-
waitError(page, act, error, 'URL');
|
|
693
|
+
waitError(page, act, error, 'text in the URL');
|
|
691
694
|
}
|
|
692
695
|
}
|
|
693
696
|
// Otherwise, if the text is to be a substring of the page title:
|
|
@@ -709,7 +712,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
709
712
|
}
|
|
710
713
|
catch(error) {
|
|
711
714
|
actIndex = -2;
|
|
712
|
-
waitError(page, act, error, 'title');
|
|
715
|
+
waitError(page, act, error, 'text in the title');
|
|
713
716
|
}
|
|
714
717
|
}
|
|
715
718
|
// Otherwise, if the text is to be a substring of the text of the page body:
|
|
@@ -723,14 +726,14 @@ const doActs = async (report, actIndex, page) => {
|
|
|
723
726
|
which,
|
|
724
727
|
{
|
|
725
728
|
polling: 2000,
|
|
726
|
-
timeout:
|
|
729
|
+
timeout: 15000
|
|
727
730
|
}
|
|
728
731
|
);
|
|
729
732
|
result.found = true;
|
|
730
733
|
}
|
|
731
734
|
catch(error) {
|
|
732
735
|
actIndex = -2;
|
|
733
|
-
waitError(page, act, error, 'body');
|
|
736
|
+
waitError(page, act, error, 'text in the body');
|
|
734
737
|
}
|
|
735
738
|
}
|
|
736
739
|
}
|
|
@@ -950,80 +953,71 @@ const doActs = async (report, actIndex, page) => {
|
|
|
950
953
|
act.result = {found: false};
|
|
951
954
|
let selection = {};
|
|
952
955
|
let tries = 0;
|
|
953
|
-
const slimText = debloat(act.which);
|
|
956
|
+
const slimText = act.which ? debloat(act.which) : '';
|
|
954
957
|
while (tries++ < 5 && ! act.result.found) {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
break;
|
|
979
|
-
}
|
|
958
|
+
// If the page still exists:
|
|
959
|
+
if (page) {
|
|
960
|
+
// Identify the elements of the specified type.
|
|
961
|
+
const selections = await page.$$(selector);
|
|
962
|
+
// If there are any:
|
|
963
|
+
if (selections.length) {
|
|
964
|
+
// If there are enough to make a match possible:
|
|
965
|
+
if ((act.index || 0) < selections.length) {
|
|
966
|
+
// For each element of the specified type:
|
|
967
|
+
let matchCount = 0;
|
|
968
|
+
const selectionTexts = [];
|
|
969
|
+
for (selection of selections) {
|
|
970
|
+
// Add its text or an empty string to the list of texts of such elements.
|
|
971
|
+
const selectionText = slimText ? await textOf(page, selection) : '';
|
|
972
|
+
selectionTexts.push(selectionText);
|
|
973
|
+
// If its text includes any specified text:
|
|
974
|
+
if (selectionText.includes(slimText)) {
|
|
975
|
+
// If the element has the specified index among such elements:
|
|
976
|
+
if (matchCount++ === (act.index || 0)) {
|
|
977
|
+
// Report it as the matching element and stop checking.
|
|
978
|
+
act.result.found = true;
|
|
979
|
+
act.result.text = slimText;
|
|
980
|
+
break;
|
|
980
981
|
}
|
|
981
982
|
}
|
|
982
|
-
// If no element satisfied the specifications:
|
|
983
|
-
if (! act.result.found) {
|
|
984
|
-
// Add the failure data to the report.
|
|
985
|
-
act.result.success = false;
|
|
986
|
-
act.result.error = 'exhausted';
|
|
987
|
-
act.result.typeElementCount = selections.length;
|
|
988
|
-
if (slimText) {
|
|
989
|
-
act.result.textElementCount = --matchCount;
|
|
990
|
-
}
|
|
991
|
-
act.result.message = 'Not enough specified elements exist';
|
|
992
|
-
act.result.candidateTexts = selectionTexts;
|
|
993
|
-
}
|
|
994
983
|
}
|
|
995
|
-
//
|
|
996
|
-
|
|
984
|
+
// If no element satisfied the specifications:
|
|
985
|
+
if (! act.result.found) {
|
|
997
986
|
// Add the failure data to the report.
|
|
998
987
|
act.result.success = false;
|
|
999
|
-
act.result.error = '
|
|
988
|
+
act.result.error = 'exhausted';
|
|
1000
989
|
act.result.typeElementCount = selections.length;
|
|
1001
|
-
|
|
990
|
+
if (slimText) {
|
|
991
|
+
act.result.textElementCount = --matchCount;
|
|
992
|
+
}
|
|
993
|
+
act.result.message = 'Not enough specified elements exist';
|
|
994
|
+
act.result.candidateTexts = selectionTexts;
|
|
1002
995
|
}
|
|
1003
996
|
}
|
|
1004
|
-
// Otherwise, i.e. if there are
|
|
997
|
+
// Otherwise, i.e. if there are too few such elements to make a match possible:
|
|
1005
998
|
else {
|
|
1006
999
|
// Add the failure data to the report.
|
|
1007
1000
|
act.result.success = false;
|
|
1008
|
-
act.result.error = '
|
|
1009
|
-
act.result.typeElementCount =
|
|
1010
|
-
act.result.message = '
|
|
1001
|
+
act.result.error = 'fewer';
|
|
1002
|
+
act.result.typeElementCount = selections.length;
|
|
1003
|
+
act.result.message = 'Elements of specified type too few';
|
|
1011
1004
|
}
|
|
1012
1005
|
}
|
|
1013
|
-
// Otherwise, i.e. if
|
|
1006
|
+
// Otherwise, i.e. if there are no elements of the specified type:
|
|
1014
1007
|
else {
|
|
1015
1008
|
// Add the failure data to the report.
|
|
1016
1009
|
act.result.success = false;
|
|
1017
|
-
act.result.error = '
|
|
1018
|
-
act.result.
|
|
1010
|
+
act.result.error = 'none';
|
|
1011
|
+
act.result.typeElementCount = 0;
|
|
1012
|
+
act.result.message = 'No elements of specified type found';
|
|
1019
1013
|
}
|
|
1020
1014
|
}
|
|
1021
|
-
// Otherwise, i.e. if no
|
|
1015
|
+
// Otherwise, i.e. if the page no longer exists:
|
|
1022
1016
|
else {
|
|
1023
1017
|
// Add the failure data to the report.
|
|
1024
1018
|
act.result.success = false;
|
|
1025
|
-
act.result.error = '
|
|
1026
|
-
act.result.message = '
|
|
1019
|
+
act.result.error = 'gone';
|
|
1020
|
+
act.result.message = 'Page gone';
|
|
1027
1021
|
}
|
|
1028
1022
|
if (! act.result.found) {
|
|
1029
1023
|
await wait(2000);
|
|
@@ -1032,33 +1026,36 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1032
1026
|
// If a match was found:
|
|
1033
1027
|
if (act.result.found) {
|
|
1034
1028
|
// FUNCTION DEFINITION START
|
|
1035
|
-
// Perform a click or Enter keypress and wait for
|
|
1036
|
-
const doAndWait = async
|
|
1029
|
+
// Perform a click or Enter keypress and wait for the network to be idle.
|
|
1030
|
+
const doAndWait = async isClick => {
|
|
1031
|
+
const move = isClick ? 'click' : 'Enter keypress';
|
|
1037
1032
|
try {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
]);
|
|
1042
|
-
// Wait for the new page to load.
|
|
1043
|
-
await newPage.waitForLoadState('domcontentloaded', {timeout: 10000});
|
|
1044
|
-
// Make the new page the current page.
|
|
1045
|
-
page = newPage;
|
|
1033
|
+
await isClick
|
|
1034
|
+
? selection.click({timeout: 4000})
|
|
1035
|
+
: selection.press('Enter', {timeout: 4000});
|
|
1046
1036
|
act.result.success = true;
|
|
1047
|
-
act.result.move =
|
|
1048
|
-
act.result.newURL = page.url();
|
|
1037
|
+
act.result.move = move;
|
|
1049
1038
|
}
|
|
1050
|
-
// If the action, event, or load failed:
|
|
1051
1039
|
catch(error) {
|
|
1052
|
-
// Quit and report the failure.
|
|
1053
|
-
const action = actionIsClick ? 'clicking' : 'pressing Enter';
|
|
1054
|
-
console.log(
|
|
1055
|
-
`ERROR ${action} (${error.message.replace(/\n.+/s, '')})`
|
|
1056
|
-
);
|
|
1057
1040
|
act.result.success = false;
|
|
1058
1041
|
act.result.error = 'moveFailure';
|
|
1059
|
-
act.result.message =
|
|
1042
|
+
act.result.message = `ERROR: ${move} failed`;
|
|
1043
|
+
console.log(`ERROR: ${move} failed (${errorStart(error)})`);
|
|
1060
1044
|
actIndex = -2;
|
|
1061
1045
|
}
|
|
1046
|
+
if (act.result.success) {
|
|
1047
|
+
try {
|
|
1048
|
+
await page.context().waitForEvent('networkidle', {timeout: 10000});
|
|
1049
|
+
act.result.idleTimely = true;
|
|
1050
|
+
}
|
|
1051
|
+
catch(error) {
|
|
1052
|
+
console.log(`ERROR: Network busy after ${move} (${errorStart(error)})`);
|
|
1053
|
+
act.result.idleTimely = false;
|
|
1054
|
+
}
|
|
1055
|
+
// If the move created a new page, make it current.
|
|
1056
|
+
page = currentPage;
|
|
1057
|
+
act.result.newURL = page.url();
|
|
1058
|
+
}
|
|
1062
1059
|
};
|
|
1063
1060
|
// FUNCTION DEFINITION END
|
|
1064
1061
|
// If the move is a button click, perform it.
|
|
@@ -1114,7 +1111,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1114
1111
|
act.result.target = target || 'DEFAULT';
|
|
1115
1112
|
// If the destination is a new page:
|
|
1116
1113
|
if (target && target !== '_self') {
|
|
1117
|
-
// Click the link and wait for the
|
|
1114
|
+
// Click the link and wait for the network to be idle.
|
|
1118
1115
|
doAndWait(true);
|
|
1119
1116
|
}
|
|
1120
1117
|
// Otherwise, i.e. if the destination is in the current page:
|
|
@@ -1131,9 +1128,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1131
1128
|
// If the click or load failed:
|
|
1132
1129
|
catch(error) {
|
|
1133
1130
|
// Quit and report the failure.
|
|
1134
|
-
console.log(
|
|
1135
|
-
`ERROR clicking link (${error.message.replace(/\n.+/s, '')})`
|
|
1136
|
-
);
|
|
1131
|
+
console.log(`ERROR clicking link (${errorStart(error)})`);
|
|
1137
1132
|
act.result.success = false;
|
|
1138
1133
|
act.result.error = 'unclickable';
|
|
1139
1134
|
act.result.message = 'ERROR: click or load timed out';
|
|
@@ -1171,6 +1166,14 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1171
1166
|
}
|
|
1172
1167
|
// Otherwise, if it is entering text on a text- or search-input element:
|
|
1173
1168
|
else if (['text', 'search'].includes(act.type)) {
|
|
1169
|
+
act.result.attributes = {};
|
|
1170
|
+
const {attributes} = act.result;
|
|
1171
|
+
const type = await selection.getAttribute('type');
|
|
1172
|
+
const label = await selection.getAttribute('aria-label');
|
|
1173
|
+
const labelRefs = await selection.getAttribute('aria-labelledby');
|
|
1174
|
+
attributes.type = type || '';
|
|
1175
|
+
attributes.label = label || '';
|
|
1176
|
+
attributes.labelRefs = labelRefs || '';
|
|
1174
1177
|
// If the text contains a placeholder for an environment variable:
|
|
1175
1178
|
let {what} = act;
|
|
1176
1179
|
if (/__[A-Z]+__/.test(what)) {
|
|
@@ -1186,7 +1189,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1186
1189
|
act.result.move = 'entered';
|
|
1187
1190
|
// If the input is a search input:
|
|
1188
1191
|
if (act.type === 'search') {
|
|
1189
|
-
// Press the Enter key and wait for a
|
|
1192
|
+
// Press the Enter key and wait for a network to be idle.
|
|
1190
1193
|
doAndWait(false);
|
|
1191
1194
|
}
|
|
1192
1195
|
}
|
package/tests/textNodes.js
CHANGED
|
@@ -15,6 +15,15 @@ exports.reporter = async (page, detailLevel, text) => {
|
|
|
15
15
|
const matchNodes = [];
|
|
16
16
|
// Normalize the body.
|
|
17
17
|
document.body.normalize();
|
|
18
|
+
// Make a copy of the body.
|
|
19
|
+
const tempBody = document.body.cloneNode(true);
|
|
20
|
+
// Insert it into the document.
|
|
21
|
+
document.body.appendChild(tempBody);
|
|
22
|
+
// Remove the irrelevant text content from the copy.
|
|
23
|
+
const extraElements = Array.from(tempBody.querySelectorAll('style, script, svg'));
|
|
24
|
+
extraElements.forEach(element => {
|
|
25
|
+
element.textContent = '';
|
|
26
|
+
});
|
|
18
27
|
// FUNCTION DEFINITIONS START
|
|
19
28
|
// Compacts a string.
|
|
20
29
|
const compact = string => string.replace(/\s+/g, ' ').trim();
|
|
@@ -26,9 +35,9 @@ exports.reporter = async (page, detailLevel, text) => {
|
|
|
26
35
|
const data = {
|
|
27
36
|
tagName: element.tagName
|
|
28
37
|
};
|
|
29
|
-
if (! ['SCRIPT', 'SVG', 'svg'].includes(element.tagName)) {
|
|
38
|
+
if (! ['STYLE', 'SCRIPT', 'SVG', 'svg'].includes(element.tagName)) {
|
|
30
39
|
if (withText) {
|
|
31
|
-
data.text = element.textContent;
|
|
40
|
+
data.text = compact(element.textContent);
|
|
32
41
|
}
|
|
33
42
|
// Add data on its attributes, if any, to the data.
|
|
34
43
|
const {attributes} = element;
|
|
@@ -73,16 +82,13 @@ exports.reporter = async (page, detailLevel, text) => {
|
|
|
73
82
|
// FUNCTION DEFINITIONS END
|
|
74
83
|
const normText = normalize(text);
|
|
75
84
|
// Create a collection of the text nodes.
|
|
76
|
-
const walker = document.createTreeWalker(
|
|
85
|
+
const walker = document.createTreeWalker(tempBody, NodeFilter.SHOW_TEXT);
|
|
77
86
|
// Get their count.
|
|
78
87
|
const data = {nodeCount: 0};
|
|
79
88
|
let more = true;
|
|
80
89
|
while(more) {
|
|
81
90
|
if (walker.nextNode()) {
|
|
82
|
-
if (
|
|
83
|
-
normalize(walker.currentNode.nodeValue).includes(normText)
|
|
84
|
-
&& walker.currentNode.parentElement.tagName !== 'SCRIPT'
|
|
85
|
-
) {
|
|
91
|
+
if (normalize(walker.currentNode.nodeValue).includes(normText)) {
|
|
86
92
|
data.nodeCount++;
|
|
87
93
|
matchNodes.push(walker.currentNode);
|
|
88
94
|
}
|
|
@@ -120,6 +126,7 @@ exports.reporter = async (page, detailLevel, text) => {
|
|
|
120
126
|
data.items.push(itemData);
|
|
121
127
|
});
|
|
122
128
|
}
|
|
129
|
+
document.body.removeChild(tempBody);
|
|
123
130
|
return data;
|
|
124
131
|
}, [detailLevel, text]);
|
|
125
132
|
}
|