testaro 5.7.6 → 5.9.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/commands.js +9 -0
- package/package.json +1 -1
- package/run.js +81 -49
- package/tests/elements.js +130 -0
- package/tests/title.js +8 -0
package/commands.js
CHANGED
|
@@ -150,6 +150,15 @@ exports.commands = {
|
|
|
150
150
|
rules: [true, 'array', 'areStrings', 'rule names, or empty if all']
|
|
151
151
|
}
|
|
152
152
|
],
|
|
153
|
+
elements: [
|
|
154
|
+
'Perform an elements test',
|
|
155
|
+
{
|
|
156
|
+
detailLevel: [true, 'number', '', '0 = counts, 1 = selves, 2 = also sibling nodes'],
|
|
157
|
+
tagName: [false, 'string', '', 'tag name of elements'],
|
|
158
|
+
onlyVisible: [false, 'boolean', '', 'whether to exclude invisible elements'],
|
|
159
|
+
attribute: [false, 'string', 'hasLength', 'required attribute or attribute=value']
|
|
160
|
+
}
|
|
161
|
+
],
|
|
153
162
|
embAc: [
|
|
154
163
|
'Perform an embAc test',
|
|
155
164
|
{
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -34,6 +34,7 @@ const tests = {
|
|
|
34
34
|
bulk: 'count of visible elements',
|
|
35
35
|
continuum: 'Level Access Continuum, community edition',
|
|
36
36
|
docType: 'document without a doctype property',
|
|
37
|
+
elements: 'data on specified elements',
|
|
37
38
|
embAc: 'active elements embedded in links or buttons',
|
|
38
39
|
focAll: 'focusable and Tab-focused elements',
|
|
39
40
|
focInd: 'focus indicators',
|
|
@@ -55,6 +56,7 @@ const tests = {
|
|
|
55
56
|
styleDiff: 'style inconsistencies',
|
|
56
57
|
tabNav: 'keyboard navigation between tab elements',
|
|
57
58
|
tenon: 'Tenon',
|
|
59
|
+
title: 'page title',
|
|
58
60
|
titledEl: 'title attributes on inappropriate elements',
|
|
59
61
|
wave: 'WAVE',
|
|
60
62
|
zIndex: 'z indexes'
|
|
@@ -81,6 +83,7 @@ const errorWords = [
|
|
|
81
83
|
'content security policy',
|
|
82
84
|
'deprecated',
|
|
83
85
|
'error',
|
|
86
|
+
'expected',
|
|
84
87
|
'failed',
|
|
85
88
|
'invalid',
|
|
86
89
|
'missing',
|
|
@@ -442,7 +445,7 @@ const textOf = async (page, element) => {
|
|
|
442
445
|
return null;
|
|
443
446
|
}
|
|
444
447
|
};
|
|
445
|
-
// Returns an element of a type case-insensitively
|
|
448
|
+
// Returns an element of a type case-insensitively including a text.
|
|
446
449
|
const matchElement = async (page, selector, matchText, index = 0) => {
|
|
447
450
|
if (matchText) {
|
|
448
451
|
// If the page still exists:
|
|
@@ -454,14 +457,18 @@ const matchElement = async (page, selector, matchText, index = 0) => {
|
|
|
454
457
|
if (selections.length) {
|
|
455
458
|
// If there are enough to make a match possible:
|
|
456
459
|
if (index < selections.length) {
|
|
457
|
-
//
|
|
460
|
+
// For each element of the specified type:
|
|
458
461
|
let matchCount = 0;
|
|
459
462
|
const selectionTexts = [];
|
|
460
463
|
for (const selection of selections) {
|
|
464
|
+
// Add its text to the list of texts of such elements.
|
|
461
465
|
const selectionText = await textOf(page, selection);
|
|
462
466
|
selectionTexts.push(selectionText);
|
|
467
|
+
// If its text includes the specified text:
|
|
463
468
|
if (selectionText.includes(slimText)) {
|
|
469
|
+
// If the count of such elements with such texts found so far is the specified count:
|
|
464
470
|
if (matchCount++ === index) {
|
|
471
|
+
// Return it as the matching element.
|
|
465
472
|
return {
|
|
466
473
|
success: true,
|
|
467
474
|
matchingElement: selection
|
|
@@ -661,7 +668,8 @@ const isTrue = (object, specs) => {
|
|
|
661
668
|
// Adds a wait error result to an act.
|
|
662
669
|
const waitError = (page, act, error, what) => {
|
|
663
670
|
console.log(`ERROR waiting for ${what} (${error.message})`);
|
|
664
|
-
act.result =
|
|
671
|
+
act.result.found = false;
|
|
672
|
+
act.result.url = page.url();
|
|
665
673
|
act.result.error = `ERROR waiting for ${what}`;
|
|
666
674
|
return false;
|
|
667
675
|
};
|
|
@@ -754,49 +762,57 @@ const doActs = async (report, actIndex, page) => {
|
|
|
754
762
|
// Otherwise, if the act is a wait for text:
|
|
755
763
|
else if (act.type === 'wait') {
|
|
756
764
|
const {what, which} = act;
|
|
757
|
-
console.log(`>>
|
|
758
|
-
|
|
759
|
-
if
|
|
760
|
-
|
|
761
|
-
|
|
765
|
+
console.log(`>> ${what}`);
|
|
766
|
+
const result = act.result = {};
|
|
767
|
+
// Wait for the specified text, and quit if it does not appear.
|
|
768
|
+
if (what === 'url') {
|
|
769
|
+
try {
|
|
770
|
+
await page.waitForURL(which, {timeout: 15000});
|
|
771
|
+
result.found = true;
|
|
772
|
+
result.url = page.url();
|
|
773
|
+
}
|
|
774
|
+
catch(error) {
|
|
762
775
|
actIndex = -2;
|
|
763
776
|
waitError(page, act, error, 'URL');
|
|
764
|
-
}
|
|
777
|
+
}
|
|
765
778
|
}
|
|
766
|
-
else if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
779
|
+
else if (what === 'title') {
|
|
780
|
+
try {
|
|
781
|
+
await page.waitForFunction(
|
|
782
|
+
text => document
|
|
783
|
+
&& document.title
|
|
784
|
+
&& document.title.toLowerCase().includes(text.toLowerCase()),
|
|
785
|
+
which,
|
|
786
|
+
{
|
|
787
|
+
polling: 1000,
|
|
788
|
+
timeout: 5000
|
|
789
|
+
}
|
|
790
|
+
);
|
|
791
|
+
result.found = true;
|
|
792
|
+
result.title = await page.title();
|
|
793
|
+
}
|
|
794
|
+
catch(error) {
|
|
776
795
|
actIndex = -2;
|
|
777
796
|
waitError(page, act, error, 'title');
|
|
778
|
-
}
|
|
797
|
+
}
|
|
779
798
|
}
|
|
780
|
-
else if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
799
|
+
else if (what === 'body') {
|
|
800
|
+
try {
|
|
801
|
+
await page.waitForFunction(
|
|
802
|
+
text => document
|
|
803
|
+
&& document.body
|
|
804
|
+
&& document.body.innerText.toLowerCase().includes(text.toLowerCase()),
|
|
805
|
+
which,
|
|
806
|
+
{
|
|
807
|
+
polling: 2000,
|
|
808
|
+
timeout: 10000
|
|
809
|
+
}
|
|
810
|
+
);
|
|
811
|
+
result.found = true;
|
|
812
|
+
}
|
|
813
|
+
catch(error) {
|
|
790
814
|
actIndex = -2;
|
|
791
815
|
waitError(page, act, error, 'body');
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
// If the text was found:
|
|
795
|
-
if (actIndex > -2) {
|
|
796
|
-
// Add this to the report.
|
|
797
|
-
act.result = {url: page.url()};
|
|
798
|
-
if (act.what === 'title') {
|
|
799
|
-
act.result.title = await page.title();
|
|
800
816
|
}
|
|
801
817
|
}
|
|
802
818
|
}
|
|
@@ -1054,24 +1070,36 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1054
1070
|
await matchingElement.focus({timeout: 2000});
|
|
1055
1071
|
act.result = 'focused';
|
|
1056
1072
|
}
|
|
1057
|
-
// Otherwise, if it is clicking a link
|
|
1073
|
+
// Otherwise, if it is clicking a link:
|
|
1058
1074
|
else if (act.type === 'link') {
|
|
1075
|
+
// Try to click it.
|
|
1059
1076
|
const href = await matchingElement.getAttribute('href');
|
|
1060
1077
|
const target = await matchingElement.getAttribute('target');
|
|
1061
|
-
await matchingElement.click({timeout:
|
|
1062
|
-
|
|
1063
|
-
|
|
1078
|
+
await matchingElement.click({timeout: 3000})
|
|
1079
|
+
// If it cannot be clicked within 3 seconds:
|
|
1080
|
+
.catch(async error => {
|
|
1081
|
+
// Try to force-click it without actionability checks.
|
|
1082
|
+
const errorSummary = error.message.replace(/\n.+/, '');
|
|
1083
|
+
console.log(`ERROR: Link to ${href} not clickable (${errorSummary})`);
|
|
1064
1084
|
await matchingElement.click({
|
|
1065
|
-
force: true
|
|
1066
|
-
timeout: 10000
|
|
1085
|
+
force: true
|
|
1067
1086
|
})
|
|
1068
|
-
|
|
1087
|
+
// If it cannot be force-clicked:
|
|
1088
|
+
.catch(error => {
|
|
1089
|
+
// Quit and report the failure.
|
|
1069
1090
|
actIndex = -2;
|
|
1070
|
-
|
|
1071
|
-
|
|
1091
|
+
const errorSummary = error.message.replace(/\n.+/, '');
|
|
1092
|
+
console.log(`ERROR: Link to ${href} not force-clickable (${errorSummary})`);
|
|
1093
|
+
act.result = {
|
|
1094
|
+
href: href || 'NONE',
|
|
1095
|
+
target: target || 'NONE',
|
|
1096
|
+
error: 'ERROR: Normal and forced attempts to click link timed out'
|
|
1097
|
+
};
|
|
1072
1098
|
});
|
|
1073
1099
|
});
|
|
1100
|
+
// If it was clicked:
|
|
1074
1101
|
if (actIndex > -2) {
|
|
1102
|
+
// Report the success.
|
|
1075
1103
|
act.result = {
|
|
1076
1104
|
href: href || 'NONE',
|
|
1077
1105
|
target: target || 'NONE',
|
|
@@ -1089,7 +1117,9 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1089
1117
|
const optionText = await option.textContent();
|
|
1090
1118
|
optionTexts.push(optionText);
|
|
1091
1119
|
}
|
|
1092
|
-
const matchTexts = optionTexts.map(
|
|
1120
|
+
const matchTexts = optionTexts.map(
|
|
1121
|
+
(text, index) => text.includes(act.what) ? index : -1
|
|
1122
|
+
);
|
|
1093
1123
|
const index = matchTexts.filter(text => text > -1)[act.index || 0];
|
|
1094
1124
|
if (index !== undefined) {
|
|
1095
1125
|
await matchingElement.selectOption({index});
|
|
@@ -1185,7 +1215,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1185
1215
|
}
|
|
1186
1216
|
// If there is a current element:
|
|
1187
1217
|
if (currentElement) {
|
|
1188
|
-
// If it was already reached within this
|
|
1218
|
+
// If it was already reached within this act:
|
|
1189
1219
|
if (currentElement.dataset.pressesReached === actCount.toString(10)) {
|
|
1190
1220
|
// Report the error.
|
|
1191
1221
|
console.log(`ERROR: ${currentElement.tagName} element reached again`);
|
|
@@ -1319,6 +1349,8 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1319
1349
|
const errorMsg = `ERROR: Invalid command of type ${act.type}`;
|
|
1320
1350
|
act.result = errorMsg;
|
|
1321
1351
|
console.log(errorMsg);
|
|
1352
|
+
// Quit.
|
|
1353
|
+
actIndex = -2;
|
|
1322
1354
|
}
|
|
1323
1355
|
// Perform the remaining acts.
|
|
1324
1356
|
await doActs(report, actIndex + 1, page);
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/*
|
|
2
|
+
elements
|
|
3
|
+
This test reports data about specified elements.
|
|
4
|
+
*/
|
|
5
|
+
exports.reporter = async (page, detailLevel, tagName, onlyVisible, attribute) => {
|
|
6
|
+
// Determine a selector of the specified elements.
|
|
7
|
+
let selector = tagName || '*';
|
|
8
|
+
if (attribute) {
|
|
9
|
+
selector += `[${attribute}]`;
|
|
10
|
+
}
|
|
11
|
+
if (onlyVisible) {
|
|
12
|
+
selector += ':visible';
|
|
13
|
+
}
|
|
14
|
+
// Get the data on the elements.
|
|
15
|
+
const data = await page.$$eval(selector, (elements, detailLevel) => {
|
|
16
|
+
// FUNCTION DEFINITIONS START
|
|
17
|
+
// Compacts a string.
|
|
18
|
+
const compact = string => string.replace(/\s+/g, ' ').trim();
|
|
19
|
+
// Gets data on the sibling nodes of an element.
|
|
20
|
+
const getSibInfo = (node, nodeType, text) => {
|
|
21
|
+
const sibInfo = {
|
|
22
|
+
type: nodeType
|
|
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)
|
|
56
|
+
};
|
|
57
|
+
// For each of its attributes:
|
|
58
|
+
for (const attribute of element.attributes) {
|
|
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
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// If the element has text content:
|
|
82
|
+
const {labels, textContent} = element;
|
|
83
|
+
const compactContent = compact(textContent);
|
|
84
|
+
if (compactContent) {
|
|
85
|
+
// Add it to the element data.
|
|
86
|
+
datum.textContent = compactContent;
|
|
87
|
+
}
|
|
88
|
+
// If the element has labels:
|
|
89
|
+
if (labels && labels.length) {
|
|
90
|
+
// Add their texts to the element data.
|
|
91
|
+
datum.labels = Array.from(labels).map(label => compact(label.textContent));
|
|
92
|
+
}
|
|
93
|
+
// If sibling itemization is required:
|
|
94
|
+
if (detailLevel === 2) {
|
|
95
|
+
// Add the sibling data to the element data.
|
|
96
|
+
datum.siblings = {
|
|
97
|
+
before: [],
|
|
98
|
+
after: []
|
|
99
|
+
};
|
|
100
|
+
let more = element;
|
|
101
|
+
while (more) {
|
|
102
|
+
more = more.previousSibling;
|
|
103
|
+
if (more) {
|
|
104
|
+
const {nodeType, nodeValue} = more;
|
|
105
|
+
if (! (nodeType === 3 && nodeValue === '')) {
|
|
106
|
+
const sibInfo = getSibInfo(more, nodeType, nodeValue);
|
|
107
|
+
datum.siblings.before.unshift(sibInfo);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
more = element;
|
|
112
|
+
while (more) {
|
|
113
|
+
more = more.nextSibling;
|
|
114
|
+
if (more) {
|
|
115
|
+
const {nodeType, textContent} = more;
|
|
116
|
+
if (! (nodeType === 3 && textContent === '')) {
|
|
117
|
+
const sibInfo = getSibInfo(more, nodeType, compact(textContent));
|
|
118
|
+
datum.siblings.after.push(sibInfo);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
data.items.push(datum);
|
|
124
|
+
});
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
}, detailLevel);
|
|
128
|
+
// Return the result.
|
|
129
|
+
return {result: data};
|
|
130
|
+
};
|