testaro 5.11.2 → 5.12.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 +22 -7
- package/package.json +1 -1
- package/run.js +54 -37
- package/tests/elements.js +8 -1
- package/tests/textNodes.js +125 -0
- package/tests/title.js +4 -1
package/commands.js
CHANGED
|
@@ -3,7 +3,7 @@ exports.commands = {
|
|
|
3
3
|
button: [
|
|
4
4
|
'Click a button or submit input',
|
|
5
5
|
{
|
|
6
|
-
which: [
|
|
6
|
+
which: [false, 'string', 'hasLength', 'substring of button text'],
|
|
7
7
|
index: [false, 'number', '', 'index among matches if not 0'],
|
|
8
8
|
what: [false, 'string', 'hasLength', 'comment']
|
|
9
9
|
}
|
|
@@ -21,7 +21,7 @@ exports.commands = {
|
|
|
21
21
|
{
|
|
22
22
|
what: [true, 'string', 'isFocusable', 'selector of element to be focused'],
|
|
23
23
|
index: [false, 'number', '', 'index among matches if not 0'],
|
|
24
|
-
which: [
|
|
24
|
+
which: [false, 'string', 'hasLength', 'substring of element text']
|
|
25
25
|
}
|
|
26
26
|
],
|
|
27
27
|
launch: [
|
|
@@ -34,7 +34,7 @@ exports.commands = {
|
|
|
34
34
|
link: [
|
|
35
35
|
'Click a link and wait for the page to be idle or loaded',
|
|
36
36
|
{
|
|
37
|
-
which: [
|
|
37
|
+
which: [false, 'string', 'hasLength', 'substring of link text'],
|
|
38
38
|
index: [false, 'number', '', 'index among matches if not 0'],
|
|
39
39
|
what: [false, 'string', 'hasLength', 'comment']
|
|
40
40
|
}
|
|
@@ -89,10 +89,18 @@ exports.commands = {
|
|
|
89
89
|
what: [false, 'string', 'hasLength', 'comment']
|
|
90
90
|
}
|
|
91
91
|
],
|
|
92
|
+
search: [
|
|
93
|
+
'Enter text into a search input, optionally with 1 placeholder for an all-caps literal environment variable',
|
|
94
|
+
{
|
|
95
|
+
which: [false, 'string', 'hasLength', 'substring of input text'],
|
|
96
|
+
index: [false, 'number', '', 'index among matches if not 0'],
|
|
97
|
+
what: [true, 'string', 'hasLength', 'text to enter, with optional __PLACEHOLDER__']
|
|
98
|
+
}
|
|
99
|
+
],
|
|
92
100
|
select: [
|
|
93
101
|
'Select a select option',
|
|
94
102
|
{
|
|
95
|
-
which: [
|
|
103
|
+
which: [false, 'string', 'hasLength', 'substring of select-list text'],
|
|
96
104
|
index: [false, 'number', '', 'index among matches if not 0'],
|
|
97
105
|
what: [true, 'string', 'hasLength', 'substring of option text content']
|
|
98
106
|
}
|
|
@@ -122,7 +130,7 @@ exports.commands = {
|
|
|
122
130
|
text: [
|
|
123
131
|
'Enter text into a text input, optionally with 1 placeholder for an all-caps literal environment variable',
|
|
124
132
|
{
|
|
125
|
-
which: [
|
|
133
|
+
which: [false, 'string', 'hasLength', 'substring of input text'],
|
|
126
134
|
index: [false, 'number', '', 'index among matches if not 0'],
|
|
127
135
|
what: [true, 'string', 'hasLength', 'text to enter, with optional __PLACEHOLDER__']
|
|
128
136
|
}
|
|
@@ -154,9 +162,9 @@ exports.commands = {
|
|
|
154
162
|
'Perform an elements test',
|
|
155
163
|
{
|
|
156
164
|
detailLevel: [true, 'number', '', '0 to 3, to specify the level of detail'],
|
|
157
|
-
tagName: [false, 'string', '', 'tag name of elements'],
|
|
165
|
+
tagName: [false, 'string', 'hasLength', 'tag name of elements'],
|
|
158
166
|
onlyVisible: [false, 'boolean', '', 'whether to exclude invisible elements'],
|
|
159
|
-
attribute: [false, 'string', 'hasLength', 'required attribute
|
|
167
|
+
attribute: [false, 'string', 'hasLength', 'required attribute selector']
|
|
160
168
|
}
|
|
161
169
|
],
|
|
162
170
|
embAc: [
|
|
@@ -273,6 +281,13 @@ exports.commands = {
|
|
|
273
281
|
id: [true, 'string', 'hasLength', 'ID of the requested test instance']
|
|
274
282
|
}
|
|
275
283
|
],
|
|
284
|
+
textNodes: [
|
|
285
|
+
'Perform a textNodes test',
|
|
286
|
+
{
|
|
287
|
+
detailLevel: [true, 'number', '', '0 to 3, to specify the level of detail'],
|
|
288
|
+
text: [false, 'string', 'hasLength', 'case-insensiteve substring of the text node']
|
|
289
|
+
}
|
|
290
|
+
],
|
|
276
291
|
titledEl: [
|
|
277
292
|
'Perform a titledEl test',
|
|
278
293
|
{
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -24,6 +24,7 @@ const moves = {
|
|
|
24
24
|
focus: true,
|
|
25
25
|
link: 'a, [role=link]',
|
|
26
26
|
radio: 'input[type=radio]',
|
|
27
|
+
search: 'input[type=search]',
|
|
27
28
|
select: 'select',
|
|
28
29
|
text: 'input[type=text]'
|
|
29
30
|
};
|
|
@@ -56,6 +57,7 @@ const tests = {
|
|
|
56
57
|
styleDiff: 'style inconsistencies',
|
|
57
58
|
tabNav: 'keyboard navigation between tab elements',
|
|
58
59
|
tenon: 'Tenon',
|
|
60
|
+
textNodes: 'data on specified text nodes',
|
|
59
61
|
title: 'page title',
|
|
60
62
|
titledEl: 'title attributes on inappropriate elements',
|
|
61
63
|
wave: 'WAVE',
|
|
@@ -963,10 +965,10 @@ const doActs = async (report, actIndex, page) => {
|
|
|
963
965
|
let matchCount = 0;
|
|
964
966
|
const selectionTexts = [];
|
|
965
967
|
for (selection of selections) {
|
|
966
|
-
// Add its text to the list of texts of such elements.
|
|
967
|
-
const selectionText = await textOf(page, selection);
|
|
968
|
+
// Add its text or an empty string to the list of texts of such elements.
|
|
969
|
+
const selectionText = slimText ? await textOf(page, selection) : '';
|
|
968
970
|
selectionTexts.push(selectionText);
|
|
969
|
-
// If its text includes
|
|
971
|
+
// If its text includes any specified text:
|
|
970
972
|
if (selectionText.includes(slimText)) {
|
|
971
973
|
// If the element has the specified index among such elements:
|
|
972
974
|
if (matchCount++ === (act.index || 0)) {
|
|
@@ -979,17 +981,20 @@ const doActs = async (report, actIndex, page) => {
|
|
|
979
981
|
}
|
|
980
982
|
// If no element satisfied the specifications:
|
|
981
983
|
if (! act.result.found) {
|
|
984
|
+
// Add the failure data to the report.
|
|
982
985
|
act.result.success = false;
|
|
983
986
|
act.result.error = 'exhausted';
|
|
984
987
|
act.result.typeElementCount = selections.length;
|
|
985
|
-
|
|
986
|
-
|
|
988
|
+
if (slimText) {
|
|
989
|
+
act.result.textElementCount = --matchCount;
|
|
990
|
+
}
|
|
991
|
+
act.result.message = 'Not enough specified elements exist';
|
|
987
992
|
act.result.candidateTexts = selectionTexts;
|
|
988
993
|
}
|
|
989
994
|
}
|
|
990
995
|
// Otherwise, i.e. if there are too few such elements to make a match possible:
|
|
991
996
|
else {
|
|
992
|
-
//
|
|
997
|
+
// Add the failure data to the report.
|
|
993
998
|
act.result.success = false;
|
|
994
999
|
act.result.error = 'fewer';
|
|
995
1000
|
act.result.typeElementCount = selections.length;
|
|
@@ -998,16 +1003,16 @@ const doActs = async (report, actIndex, page) => {
|
|
|
998
1003
|
}
|
|
999
1004
|
// Otherwise, i.e. if there are no elements of the specified type:
|
|
1000
1005
|
else {
|
|
1001
|
-
//
|
|
1006
|
+
// Add the failure data to the report.
|
|
1002
1007
|
act.result.success = false;
|
|
1003
1008
|
act.result.error = 'none';
|
|
1004
1009
|
act.result.typeElementCount = 0;
|
|
1005
|
-
act.result.message = 'No elements specified type found';
|
|
1010
|
+
act.result.message = 'No elements of specified type found';
|
|
1006
1011
|
}
|
|
1007
1012
|
}
|
|
1008
1013
|
// Otherwise, i.e. if the page no longer exists:
|
|
1009
1014
|
else {
|
|
1010
|
-
//
|
|
1015
|
+
// Add the failure data to the report.
|
|
1011
1016
|
act.result.success = false;
|
|
1012
1017
|
act.result.error = 'gone';
|
|
1013
1018
|
act.result.message = 'Page gone';
|
|
@@ -1015,7 +1020,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1015
1020
|
}
|
|
1016
1021
|
// Otherwise, i.e. if no text was specified:
|
|
1017
1022
|
else {
|
|
1018
|
-
//
|
|
1023
|
+
// Add the failure data to the report.
|
|
1019
1024
|
act.result.success = false;
|
|
1020
1025
|
act.result.error = 'text';
|
|
1021
1026
|
act.result.message = 'No text specified';
|
|
@@ -1026,6 +1031,36 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1026
1031
|
}
|
|
1027
1032
|
// If a match was found:
|
|
1028
1033
|
if (act.result.found) {
|
|
1034
|
+
// FUNCTION DEFINITION START
|
|
1035
|
+
// Perform a click or Enter keypress and wait for a page load.
|
|
1036
|
+
const doAndWait = async actionIsClick => {
|
|
1037
|
+
try {
|
|
1038
|
+
const [newPage] = await Promise.all([
|
|
1039
|
+
page.context().waitForEvent('page', {timeout: 7000}),
|
|
1040
|
+
actionIsClick ? selection.click({timeout: 4000}) : selection.press('Enter')
|
|
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;
|
|
1046
|
+
act.result.success = true;
|
|
1047
|
+
act.result.move = actionIsClick ? 'clicked' : 'Enter pressed';
|
|
1048
|
+
act.result.newURL = page.url();
|
|
1049
|
+
}
|
|
1050
|
+
// If the action, event, or load failed:
|
|
1051
|
+
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
|
+
act.result.success = false;
|
|
1058
|
+
act.result.error = 'moveFailure';
|
|
1059
|
+
act.result.message = 'ERROR: move, navigation, or load timed out';
|
|
1060
|
+
actIndex = -2;
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
// FUNCTION DEFINITION END
|
|
1029
1064
|
// If the move is a button click, perform it.
|
|
1030
1065
|
if (act.type === 'button') {
|
|
1031
1066
|
await selection.click({timeout: 3000});
|
|
@@ -1080,30 +1115,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1080
1115
|
// If the destination is a new page:
|
|
1081
1116
|
if (target && target !== '_self') {
|
|
1082
1117
|
// Click the link and wait for the resulting page event.
|
|
1083
|
-
|
|
1084
|
-
const [newPage] = await Promise.all([
|
|
1085
|
-
page.context().waitForEvent('page', {timeout: 6000}),
|
|
1086
|
-
selection.click({timeout: 5000})
|
|
1087
|
-
]);
|
|
1088
|
-
// Wait for the new page to load.
|
|
1089
|
-
await newPage.waitForLoadState('domcontentloaded', {timeout: 10000});
|
|
1090
|
-
// Make the new page the current page.
|
|
1091
|
-
page = newPage;
|
|
1092
|
-
act.result.success = true;
|
|
1093
|
-
act.result.move = 'clicked';
|
|
1094
|
-
act.result.newURL = page.url();
|
|
1095
|
-
}
|
|
1096
|
-
// If the click, event, or load failed:
|
|
1097
|
-
catch(error) {
|
|
1098
|
-
// Quit and report the failure.
|
|
1099
|
-
console.log(
|
|
1100
|
-
`ERROR clicking new-page link (${error.message.replace(/\n.+/s, '')})`
|
|
1101
|
-
);
|
|
1102
|
-
act.result.success = false;
|
|
1103
|
-
act.result.error = 'unclickable';
|
|
1104
|
-
act.result.message = 'ERROR: click, navigation, or load timed out';
|
|
1105
|
-
actIndex = -2;
|
|
1106
|
-
}
|
|
1118
|
+
doAndWait(true);
|
|
1107
1119
|
}
|
|
1108
1120
|
// Otherwise, i.e. if the destination is in the current page:
|
|
1109
1121
|
else {
|
|
@@ -1111,7 +1123,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1111
1123
|
try {
|
|
1112
1124
|
await selection.click({timeout: 5000});
|
|
1113
1125
|
// Wait for the new content to load.
|
|
1114
|
-
await page.waitForLoadState('domcontentloaded', {timeout:
|
|
1126
|
+
await page.waitForLoadState('domcontentloaded', {timeout: 6000});
|
|
1115
1127
|
act.result.success = true;
|
|
1116
1128
|
act.result.move = 'clicked';
|
|
1117
1129
|
act.result.newURL = page.url();
|
|
@@ -1157,8 +1169,8 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1157
1169
|
act.result.move = 'selected';
|
|
1158
1170
|
act.result.option = optionText;
|
|
1159
1171
|
}
|
|
1160
|
-
// Otherwise, if it is entering text on
|
|
1161
|
-
else if (act.type
|
|
1172
|
+
// Otherwise, if it is entering text on a text- or search-input element:
|
|
1173
|
+
else if (['text', 'search'].includes(act.type)) {
|
|
1162
1174
|
// If the text contains a placeholder for an environment variable:
|
|
1163
1175
|
let {what} = act;
|
|
1164
1176
|
if (/__[A-Z]+__/.test(what)) {
|
|
@@ -1172,6 +1184,11 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1172
1184
|
report.presses += act.what.length;
|
|
1173
1185
|
act.result.success = true;
|
|
1174
1186
|
act.result.move = 'entered';
|
|
1187
|
+
// If the input is a search input:
|
|
1188
|
+
if (act.type === 'search') {
|
|
1189
|
+
// Press the Enter key and wait for a new page to load.
|
|
1190
|
+
doAndWait(false);
|
|
1191
|
+
}
|
|
1175
1192
|
}
|
|
1176
1193
|
// Otherwise, i.e. if the move is unknown, add the failure to the act.
|
|
1177
1194
|
else {
|
package/tests/elements.js
CHANGED
|
@@ -23,13 +23,20 @@ exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) =>
|
|
|
23
23
|
// FUNCTION DEFINITIONS START
|
|
24
24
|
// Compacts a string.
|
|
25
25
|
const compact = string => string.replace(/\s+/g, ' ').trim();
|
|
26
|
-
// Gets data on
|
|
26
|
+
// Gets data on a sibling node of an element.
|
|
27
27
|
const getSibInfo = (node, nodeType, text) => {
|
|
28
28
|
const sibInfo = {
|
|
29
29
|
type: nodeType
|
|
30
30
|
};
|
|
31
31
|
if (nodeType === 1) {
|
|
32
32
|
sibInfo.tagName = node.tagName;
|
|
33
|
+
sibInfo.attributes = [];
|
|
34
|
+
node.attributes.forEach(attribute => {
|
|
35
|
+
sibInfo.attributes.push({
|
|
36
|
+
name: attribute.name,
|
|
37
|
+
value: attribute.value
|
|
38
|
+
});
|
|
39
|
+
});
|
|
33
40
|
}
|
|
34
41
|
else if (nodeType === 3) {
|
|
35
42
|
sibInfo.text = compact(text);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/*
|
|
2
|
+
textNodes
|
|
3
|
+
This test reports data about specified text nodes.
|
|
4
|
+
Meanings of detailLevel values:
|
|
5
|
+
0. Only total node count; no detail.
|
|
6
|
+
1+. Count of ancestry levels to provide data on (1 = text node, 2 = also parent, etc.)
|
|
7
|
+
*/
|
|
8
|
+
exports.reporter = async (page, detailLevel, text) => {
|
|
9
|
+
let data = {};
|
|
10
|
+
// Get the data on the text nodes.
|
|
11
|
+
try {
|
|
12
|
+
data = await page.evaluate(args => {
|
|
13
|
+
const detailLevel = args[0];
|
|
14
|
+
const text = args[1];
|
|
15
|
+
const matchNodes = [];
|
|
16
|
+
// Normalize the body.
|
|
17
|
+
document.body.normalize();
|
|
18
|
+
// FUNCTION DEFINITIONS START
|
|
19
|
+
// Compacts a string.
|
|
20
|
+
const compact = string => string.replace(/\s+/g, ' ').trim();
|
|
21
|
+
// Compacts and lower-cases a string.
|
|
22
|
+
const normalize = string => compact(string).toLowerCase();
|
|
23
|
+
// Gets data on an element.
|
|
24
|
+
const getElementData = (element, withText) => {
|
|
25
|
+
// Initialize the data.
|
|
26
|
+
const data = {
|
|
27
|
+
tagName: element.tagName
|
|
28
|
+
};
|
|
29
|
+
if (withText) {
|
|
30
|
+
data.text = element.textContent;
|
|
31
|
+
}
|
|
32
|
+
// Add data on its attributes, if any, to the data.
|
|
33
|
+
const {attributes} = element;
|
|
34
|
+
if (attributes) {
|
|
35
|
+
data.attributes = [];
|
|
36
|
+
for (const attribute of attributes) {
|
|
37
|
+
const {name, value} = attribute;
|
|
38
|
+
data.attributes.push({
|
|
39
|
+
name,
|
|
40
|
+
value
|
|
41
|
+
});
|
|
42
|
+
// If any attribute is a labeler reference:
|
|
43
|
+
if (name === 'aria-labelledby') {
|
|
44
|
+
// Add the label texts to the data.
|
|
45
|
+
const labelerIDs = value.split(/\s+/);
|
|
46
|
+
data.refLabels = [];
|
|
47
|
+
labelerIDs.forEach(id => {
|
|
48
|
+
const labeler = document.getElementById(id);
|
|
49
|
+
if (labeler) {
|
|
50
|
+
data.refLabels.push(compact(labeler.textContent));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Add data on its labels, if any, to the data.
|
|
57
|
+
const {labels} = element;
|
|
58
|
+
if (labels) {
|
|
59
|
+
data.labels = Array.from(labels).map(label => compact(label.textContent));
|
|
60
|
+
}
|
|
61
|
+
return data;
|
|
62
|
+
};
|
|
63
|
+
// FUNCTION DEFINITIONS END
|
|
64
|
+
const normText = normalize(text);
|
|
65
|
+
// Create a collection of the text nodes.
|
|
66
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
|
|
67
|
+
// Get their count.
|
|
68
|
+
const data = {nodeCount: 0};
|
|
69
|
+
let more = true;
|
|
70
|
+
while(more) {
|
|
71
|
+
if (walker.nextNode()) {
|
|
72
|
+
if (
|
|
73
|
+
normalize(walker.currentNode.nodeValue).includes(normText)
|
|
74
|
+
&& walker.currentNode.parentElement.tagName !== 'SCRIPT'
|
|
75
|
+
) {
|
|
76
|
+
data.nodeCount++;
|
|
77
|
+
matchNodes.push(walker.currentNode);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
more = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// If no itemization is required:
|
|
85
|
+
if (detailLevel === 0) {
|
|
86
|
+
// Return the node count.
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
// Otherwise, i.e. if itemization is required:
|
|
90
|
+
else {
|
|
91
|
+
// Initialize the item data.
|
|
92
|
+
data.items = [];
|
|
93
|
+
// For each text node matching the specified text:
|
|
94
|
+
matchNodes.forEach(node => {
|
|
95
|
+
// Initialize the data on it.
|
|
96
|
+
const itemData = {text: compact(node.nodeValue)};
|
|
97
|
+
// If ancestral itemization is required:
|
|
98
|
+
if (detailLevel > 1) {
|
|
99
|
+
// Add the ancestral data to the item data.
|
|
100
|
+
itemData.ancestors = [];
|
|
101
|
+
let base = node;
|
|
102
|
+
let currentLevel = 1;
|
|
103
|
+
while(currentLevel++ < detailLevel) {
|
|
104
|
+
const newBase = base.parentElement;
|
|
105
|
+
itemData.ancestors.push(getElementData(newBase, currentLevel > 2));
|
|
106
|
+
base = newBase;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Add the node data to the itemization.
|
|
110
|
+
data.items.push(itemData);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return data;
|
|
114
|
+
}, [detailLevel, text]);
|
|
115
|
+
}
|
|
116
|
+
catch(error) {
|
|
117
|
+
console.log(`ERROR performing test (${error.message.replace(/\n.+/s, '')})`);
|
|
118
|
+
data = {
|
|
119
|
+
prevented: true,
|
|
120
|
+
error: 'ERROR performing test'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
// Return the result.
|
|
124
|
+
return {result: data};
|
|
125
|
+
};
|