testaro 5.7.5 → 5.9.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 +9 -0
- package/package.json +1 -1
- package/run.js +80 -49
- package/tests/elements.js +128 -0
- package/tests/hover.js +181 -141
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',
|
|
@@ -81,6 +82,7 @@ const errorWords = [
|
|
|
81
82
|
'content security policy',
|
|
82
83
|
'deprecated',
|
|
83
84
|
'error',
|
|
85
|
+
'expected',
|
|
84
86
|
'failed',
|
|
85
87
|
'invalid',
|
|
86
88
|
'missing',
|
|
@@ -442,7 +444,7 @@ const textOf = async (page, element) => {
|
|
|
442
444
|
return null;
|
|
443
445
|
}
|
|
444
446
|
};
|
|
445
|
-
// Returns an element of a type case-insensitively
|
|
447
|
+
// Returns an element of a type case-insensitively including a text.
|
|
446
448
|
const matchElement = async (page, selector, matchText, index = 0) => {
|
|
447
449
|
if (matchText) {
|
|
448
450
|
// If the page still exists:
|
|
@@ -454,14 +456,18 @@ const matchElement = async (page, selector, matchText, index = 0) => {
|
|
|
454
456
|
if (selections.length) {
|
|
455
457
|
// If there are enough to make a match possible:
|
|
456
458
|
if (index < selections.length) {
|
|
457
|
-
//
|
|
459
|
+
// For each element of the specified type:
|
|
458
460
|
let matchCount = 0;
|
|
459
461
|
const selectionTexts = [];
|
|
460
462
|
for (const selection of selections) {
|
|
463
|
+
// Add its text to the list of texts of such elements.
|
|
461
464
|
const selectionText = await textOf(page, selection);
|
|
462
465
|
selectionTexts.push(selectionText);
|
|
466
|
+
// If its text includes the specified text:
|
|
463
467
|
if (selectionText.includes(slimText)) {
|
|
468
|
+
// If the count of such elements with such texts found so far is the specified count:
|
|
464
469
|
if (matchCount++ === index) {
|
|
470
|
+
// Return it as the matching element.
|
|
465
471
|
return {
|
|
466
472
|
success: true,
|
|
467
473
|
matchingElement: selection
|
|
@@ -661,7 +667,8 @@ const isTrue = (object, specs) => {
|
|
|
661
667
|
// Adds a wait error result to an act.
|
|
662
668
|
const waitError = (page, act, error, what) => {
|
|
663
669
|
console.log(`ERROR waiting for ${what} (${error.message})`);
|
|
664
|
-
act.result =
|
|
670
|
+
act.result.found = false;
|
|
671
|
+
act.result.url = page.url();
|
|
665
672
|
act.result.error = `ERROR waiting for ${what}`;
|
|
666
673
|
return false;
|
|
667
674
|
};
|
|
@@ -754,49 +761,57 @@ const doActs = async (report, actIndex, page) => {
|
|
|
754
761
|
// Otherwise, if the act is a wait for text:
|
|
755
762
|
else if (act.type === 'wait') {
|
|
756
763
|
const {what, which} = act;
|
|
757
|
-
console.log(`>>
|
|
758
|
-
|
|
759
|
-
if
|
|
760
|
-
|
|
761
|
-
|
|
764
|
+
console.log(`>> ${what}`);
|
|
765
|
+
const result = act.result = {};
|
|
766
|
+
// Wait for the specified text, and quit if it does not appear.
|
|
767
|
+
if (what === 'url') {
|
|
768
|
+
try {
|
|
769
|
+
await page.waitForURL(which, {timeout: 15000});
|
|
770
|
+
result.found = true;
|
|
771
|
+
result.url = page.url();
|
|
772
|
+
}
|
|
773
|
+
catch(error) {
|
|
762
774
|
actIndex = -2;
|
|
763
775
|
waitError(page, act, error, 'URL');
|
|
764
|
-
}
|
|
776
|
+
}
|
|
765
777
|
}
|
|
766
|
-
else if (
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
778
|
+
else if (what === 'title') {
|
|
779
|
+
try {
|
|
780
|
+
await page.waitForFunction(
|
|
781
|
+
text => document
|
|
782
|
+
&& document.title
|
|
783
|
+
&& document.title.toLowerCase().includes(text.toLowerCase()),
|
|
784
|
+
which,
|
|
785
|
+
{
|
|
786
|
+
polling: 1000,
|
|
787
|
+
timeout: 5000
|
|
788
|
+
}
|
|
789
|
+
);
|
|
790
|
+
result.found = true;
|
|
791
|
+
result.title = await page.title();
|
|
792
|
+
}
|
|
793
|
+
catch(error) {
|
|
776
794
|
actIndex = -2;
|
|
777
795
|
waitError(page, act, error, 'title');
|
|
778
|
-
}
|
|
796
|
+
}
|
|
779
797
|
}
|
|
780
|
-
else if (
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
798
|
+
else if (what === 'body') {
|
|
799
|
+
try {
|
|
800
|
+
await page.waitForFunction(
|
|
801
|
+
text => document
|
|
802
|
+
&& document.body
|
|
803
|
+
&& document.body.innerText.toLowerCase().includes(text.toLowerCase()),
|
|
804
|
+
which,
|
|
805
|
+
{
|
|
806
|
+
polling: 2000,
|
|
807
|
+
timeout: 10000
|
|
808
|
+
}
|
|
809
|
+
);
|
|
810
|
+
result.found = true;
|
|
811
|
+
}
|
|
812
|
+
catch(error) {
|
|
790
813
|
actIndex = -2;
|
|
791
814
|
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
815
|
}
|
|
801
816
|
}
|
|
802
817
|
}
|
|
@@ -1054,24 +1069,36 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1054
1069
|
await matchingElement.focus({timeout: 2000});
|
|
1055
1070
|
act.result = 'focused';
|
|
1056
1071
|
}
|
|
1057
|
-
// Otherwise, if it is clicking a link
|
|
1072
|
+
// Otherwise, if it is clicking a link:
|
|
1058
1073
|
else if (act.type === 'link') {
|
|
1074
|
+
// Try to click it.
|
|
1059
1075
|
const href = await matchingElement.getAttribute('href');
|
|
1060
1076
|
const target = await matchingElement.getAttribute('target');
|
|
1061
|
-
await matchingElement.click({timeout:
|
|
1062
|
-
|
|
1063
|
-
|
|
1077
|
+
await matchingElement.click({timeout: 3000})
|
|
1078
|
+
// If it cannot be clicked within 3 seconds:
|
|
1079
|
+
.catch(async error => {
|
|
1080
|
+
// Try to force-click it without actionability checks.
|
|
1081
|
+
const errorSummary = error.message.replace(/\n.+/, '');
|
|
1082
|
+
console.log(`ERROR: Link to ${href} not clickable (${errorSummary})`);
|
|
1064
1083
|
await matchingElement.click({
|
|
1065
|
-
force: true
|
|
1066
|
-
timeout: 10000
|
|
1084
|
+
force: true
|
|
1067
1085
|
})
|
|
1068
|
-
|
|
1086
|
+
// If it cannot be force-clicked:
|
|
1087
|
+
.catch(error => {
|
|
1088
|
+
// Quit and report the failure.
|
|
1069
1089
|
actIndex = -2;
|
|
1070
|
-
|
|
1071
|
-
|
|
1090
|
+
const errorSummary = error.message.replace(/\n.+/, '');
|
|
1091
|
+
console.log(`ERROR: Link to ${href} not force-clickable (${errorSummary})`);
|
|
1092
|
+
act.result = {
|
|
1093
|
+
href: href || 'NONE',
|
|
1094
|
+
target: target || 'NONE',
|
|
1095
|
+
error: 'ERROR: Normal and forced attempts to click link timed out'
|
|
1096
|
+
};
|
|
1072
1097
|
});
|
|
1073
1098
|
});
|
|
1099
|
+
// If it was clicked:
|
|
1074
1100
|
if (actIndex > -2) {
|
|
1101
|
+
// Report the success.
|
|
1075
1102
|
act.result = {
|
|
1076
1103
|
href: href || 'NONE',
|
|
1077
1104
|
target: target || 'NONE',
|
|
@@ -1089,7 +1116,9 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1089
1116
|
const optionText = await option.textContent();
|
|
1090
1117
|
optionTexts.push(optionText);
|
|
1091
1118
|
}
|
|
1092
|
-
const matchTexts = optionTexts.map(
|
|
1119
|
+
const matchTexts = optionTexts.map(
|
|
1120
|
+
(text, index) => text.includes(act.what) ? index : -1
|
|
1121
|
+
);
|
|
1093
1122
|
const index = matchTexts.filter(text => text > -1)[act.index || 0];
|
|
1094
1123
|
if (index !== undefined) {
|
|
1095
1124
|
await matchingElement.selectOption({index});
|
|
@@ -1185,7 +1214,7 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1185
1214
|
}
|
|
1186
1215
|
// If there is a current element:
|
|
1187
1216
|
if (currentElement) {
|
|
1188
|
-
// If it was already reached within this
|
|
1217
|
+
// If it was already reached within this act:
|
|
1189
1218
|
if (currentElement.dataset.pressesReached === actCount.toString(10)) {
|
|
1190
1219
|
// Report the error.
|
|
1191
1220
|
console.log(`ERROR: ${currentElement.tagName} element reached again`);
|
|
@@ -1319,6 +1348,8 @@ const doActs = async (report, actIndex, page) => {
|
|
|
1319
1348
|
const errorMsg = `ERROR: Invalid command of type ${act.type}`;
|
|
1320
1349
|
act.result = errorMsg;
|
|
1321
1350
|
console.log(errorMsg);
|
|
1351
|
+
// Quit.
|
|
1352
|
+
actIndex = -2;
|
|
1322
1353
|
}
|
|
1323
1354
|
// Perform the remaining acts.
|
|
1324
1355
|
await doActs(report, actIndex + 1, page);
|
|
@@ -0,0 +1,128 @@
|
|
|
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 datum = {
|
|
50
|
+
tagName: element.tagName,
|
|
51
|
+
code: compact(element.outerHTML),
|
|
52
|
+
attributes: [],
|
|
53
|
+
textContent: compact(element.textContent)
|
|
54
|
+
};
|
|
55
|
+
// For each of its attributes:
|
|
56
|
+
for (const attribute of element.attributes) {
|
|
57
|
+
// Add data on the attribute to the element data.
|
|
58
|
+
const {name, value} = attribute;
|
|
59
|
+
datum.attributes.push({
|
|
60
|
+
name,
|
|
61
|
+
value
|
|
62
|
+
});
|
|
63
|
+
// If the element has reference labels:
|
|
64
|
+
if (name === 'aria-labelledby') {
|
|
65
|
+
// Add their texts to the element data.
|
|
66
|
+
const labelerIDs = value.split(/\s+/);
|
|
67
|
+
const labelers = [];
|
|
68
|
+
labelerIDs.forEach(id => {
|
|
69
|
+
const labeler = document.getElementById(id);
|
|
70
|
+
if (labeler) {
|
|
71
|
+
labelers.push(compact(labeler.textContent));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
if (labelers.length) {
|
|
75
|
+
datum.labelers = labelers;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// If the element has text content:
|
|
80
|
+
const {labels, textContent} = element;
|
|
81
|
+
const compactContent = compact(textContent);
|
|
82
|
+
if (compactContent) {
|
|
83
|
+
// Add it to the element data.
|
|
84
|
+
datum.textContent = compactContent;
|
|
85
|
+
}
|
|
86
|
+
// If the element has labels:
|
|
87
|
+
if (labels && labels.length) {
|
|
88
|
+
// Add their texts to the element data.
|
|
89
|
+
datum.labels = Array.from(labels).map(label => compact(label.textContent));
|
|
90
|
+
}
|
|
91
|
+
// If sibling itemization is required:
|
|
92
|
+
if (detailLevel === 2) {
|
|
93
|
+
// Add the sibling data to the element data.
|
|
94
|
+
datum.siblings = {
|
|
95
|
+
before: [],
|
|
96
|
+
after: []
|
|
97
|
+
};
|
|
98
|
+
let more = element;
|
|
99
|
+
while (more) {
|
|
100
|
+
more = more.previousSibling;
|
|
101
|
+
if (more) {
|
|
102
|
+
const {nodeType, nodeValue} = more;
|
|
103
|
+
if (! (nodeType === 3 && nodeValue === '')) {
|
|
104
|
+
const sibInfo = getSibInfo(more, nodeType, nodeValue);
|
|
105
|
+
datum.siblings.before.unshift(sibInfo);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
more = element;
|
|
110
|
+
while (more) {
|
|
111
|
+
more = more.nextSibling;
|
|
112
|
+
if (more) {
|
|
113
|
+
const {nodeType, textContent} = more;
|
|
114
|
+
if (! (nodeType === 3 && textContent === '')) {
|
|
115
|
+
const sibInfo = getSibInfo(more, nodeType, compact(textContent));
|
|
116
|
+
datum.siblings.after.push(sibInfo);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
data.items.push(datum);
|
|
122
|
+
});
|
|
123
|
+
return data;
|
|
124
|
+
}
|
|
125
|
+
}, detailLevel);
|
|
126
|
+
// Return the result.
|
|
127
|
+
return {result: data};
|
|
128
|
+
};
|
package/tests/hover.js
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
value in the same location being the target.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
// VARIABLES
|
|
32
|
+
|
|
33
|
+
let hasTimedOut = false;
|
|
34
|
+
|
|
31
35
|
// FUNCTIONS
|
|
32
36
|
|
|
33
37
|
// Samples a population and returns the sample.
|
|
@@ -59,153 +63,172 @@ const textOf = async (element, limit) => {
|
|
|
59
63
|
};
|
|
60
64
|
// Recursively reports impacts of hovering over triggers.
|
|
61
65
|
const find = async (data, withItems, page, region, sample, popRatio) => {
|
|
62
|
-
// If any potential triggers remain:
|
|
63
|
-
if (sample.length) {
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
},
|
|
82
|
-
firstTrigger
|
|
83
|
-
);
|
|
84
|
-
root = rootJSHandle.asElement();
|
|
85
|
-
}
|
|
86
|
-
// Identify all the descendants of the root.
|
|
87
|
-
const preDescendants = await root.$$(':visible');
|
|
88
|
-
// Identify their opacities.
|
|
89
|
-
const preOpacities = await page.evaluate(elements => elements.map(
|
|
90
|
-
element => window.getComputedStyle(element).opacity
|
|
91
|
-
), preDescendants);
|
|
92
|
-
try {
|
|
93
|
-
// Hover over the trigger.
|
|
94
|
-
await firstTrigger.hover({
|
|
95
|
-
timeout: 500,
|
|
96
|
-
noWaitAfter: true
|
|
97
|
-
});
|
|
98
|
-
// Repeatedly seeks impacts.
|
|
99
|
-
const getImpacts = async (interval, triesLeft) => {
|
|
100
|
-
// If the allowed trial count has not yet been exhausted:
|
|
101
|
-
if (triesLeft--) {
|
|
102
|
-
// Get the collection of descendants of the root.
|
|
103
|
-
const postDescendants = await root.$$(':visible');
|
|
104
|
-
// Identify the prior descandants of the root still in existence.
|
|
105
|
-
const remainerIndexes = await page.evaluate(args => {
|
|
106
|
-
const preDescendants = args[0];
|
|
107
|
-
const postDescendants = args[1];
|
|
108
|
-
const remainerIndexes = preDescendants
|
|
109
|
-
.map((element, index) => postDescendants.includes(element) ? index : -1)
|
|
110
|
-
.filter(index => index > -1);
|
|
111
|
-
return remainerIndexes;
|
|
112
|
-
}, [preDescendants, postDescendants]);
|
|
113
|
-
// Get the count of elements added by the hover event.
|
|
114
|
-
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
115
|
-
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
116
|
-
const remainers = [];
|
|
117
|
-
for (const index of remainerIndexes) {
|
|
118
|
-
remainers.push({
|
|
119
|
-
element: preDescendants[index],
|
|
120
|
-
preOpacity: preOpacities[index],
|
|
121
|
-
postOpacity: await page.evaluate(
|
|
122
|
-
element => window.getComputedStyle(element).opacity, preDescendants[index]
|
|
123
|
-
)
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
const opacityChangers = remainers
|
|
127
|
-
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
128
|
-
const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
|
|
129
|
-
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
130
|
-
), opacityChangers) : 0;
|
|
131
|
-
if (additionCount || removalCount || opacityChangers.length) {
|
|
132
|
-
return {
|
|
133
|
-
additionCount,
|
|
134
|
-
removalCount,
|
|
135
|
-
opacityChangers,
|
|
136
|
-
opacityImpact
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
return await new Promise(resolve => {
|
|
141
|
-
setTimeout(() => {
|
|
142
|
-
resolve(getImpacts(interval, triesLeft));
|
|
143
|
-
}, interval);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
};
|
|
151
|
-
// Repeatedly seek impacts of the hover at intervals.
|
|
152
|
-
const impacts = await getImpacts(300, 4);
|
|
153
|
-
// If there were any:
|
|
154
|
-
if (impacts) {
|
|
155
|
-
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
156
|
-
await page.hover('body', {
|
|
157
|
-
position: {
|
|
158
|
-
x: 0,
|
|
159
|
-
y: 0
|
|
66
|
+
// If any potential triggers remain and the test has not timed out:
|
|
67
|
+
if (sample.length && ! hasTimedOut) {
|
|
68
|
+
// Get and report the impacts until and unless the test times out.
|
|
69
|
+
try {
|
|
70
|
+
// Identify the first of them.
|
|
71
|
+
const firstTrigger = sample[0];
|
|
72
|
+
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
73
|
+
.catch(error => '');
|
|
74
|
+
if (tagNameJSHandle) {
|
|
75
|
+
const tagName = await tagNameJSHandle.jsonValue();
|
|
76
|
+
// Identify the root of a subtree likely to contain impacted elements.
|
|
77
|
+
let root = firstTrigger;
|
|
78
|
+
if (['A', 'BUTTON', 'LI'].includes(tagName)) {
|
|
79
|
+
const rootJSHandle = await page.evaluateHandle(
|
|
80
|
+
firstTrigger => {
|
|
81
|
+
const parent = firstTrigger.parentElement || firstTrigger;
|
|
82
|
+
const grandparent = parent.parentElement || parent;
|
|
83
|
+
const greatGrandparent = grandparent.parentElement || parent;
|
|
84
|
+
return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
|
|
160
85
|
},
|
|
86
|
+
firstTrigger
|
|
87
|
+
);
|
|
88
|
+
root = rootJSHandle.asElement();
|
|
89
|
+
}
|
|
90
|
+
// Identify all the visible descendants of the root.
|
|
91
|
+
const preDescendants = await root.$$(':visible');
|
|
92
|
+
// Identify their opacities.
|
|
93
|
+
const preOpacities = await page.evaluate(elements => elements.map(
|
|
94
|
+
element => window.getComputedStyle(element).opacity
|
|
95
|
+
), preDescendants);
|
|
96
|
+
try {
|
|
97
|
+
// Hover over the trigger.
|
|
98
|
+
await firstTrigger.hover({
|
|
161
99
|
timeout: 500,
|
|
162
|
-
force: true,
|
|
163
100
|
noWaitAfter: true
|
|
164
101
|
});
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
102
|
+
// FUNCTION DEFINITION START
|
|
103
|
+
// Repeatedly seeks impacts.
|
|
104
|
+
const getImpacts = async (interval, triesLeft) => {
|
|
105
|
+
// If the allowed trial count has not yet been exhausted:
|
|
106
|
+
if (triesLeft-- && ! hasTimedOut) {
|
|
107
|
+
// Get the collection of descendants of the root.
|
|
108
|
+
const postDescendants = await root.$$(':visible');
|
|
109
|
+
// Identify the prior descandants of the root still in existence.
|
|
110
|
+
const remainerIndexes = await page.evaluate(args => {
|
|
111
|
+
const preDescendants = args[0];
|
|
112
|
+
const postDescendants = args[1];
|
|
113
|
+
const remainerIndexes = preDescendants
|
|
114
|
+
.map((element, index) => postDescendants.includes(element) ? index : -1)
|
|
115
|
+
.filter(index => index > -1);
|
|
116
|
+
return remainerIndexes;
|
|
117
|
+
}, [preDescendants, postDescendants]);
|
|
118
|
+
// Get the count of elements added by the hover event.
|
|
119
|
+
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
120
|
+
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
121
|
+
const remainers = [];
|
|
122
|
+
for (const index of remainerIndexes) {
|
|
123
|
+
remainers.push({
|
|
124
|
+
element: preDescendants[index],
|
|
125
|
+
preOpacity: preOpacities[index],
|
|
126
|
+
postOpacity: await page.evaluate(
|
|
127
|
+
element => window.getComputedStyle(element).opacity, preDescendants[index]
|
|
128
|
+
)
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const opacityChangers = remainers
|
|
132
|
+
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
133
|
+
const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
|
|
134
|
+
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
135
|
+
), opacityChangers) : 0;
|
|
136
|
+
if (additionCount || removalCount || opacityChangers.length) {
|
|
137
|
+
return {
|
|
138
|
+
additionCount,
|
|
139
|
+
removalCount,
|
|
140
|
+
opacityChangers,
|
|
141
|
+
opacityImpact
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
return await new Promise(resolve => {
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
resolve(getImpacts(interval, triesLeft));
|
|
148
|
+
}, interval);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
// FUNCTION DEFINITION END
|
|
157
|
+
// Repeatedly seek impacts of the hover at intervals.
|
|
158
|
+
const impacts = await getImpacts(300, 4);
|
|
159
|
+
// If there were any:
|
|
160
|
+
if (impacts) {
|
|
161
|
+
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
162
|
+
await page.hover('body', {
|
|
163
|
+
position: {
|
|
164
|
+
x: 0,
|
|
165
|
+
y: 0
|
|
166
|
+
},
|
|
167
|
+
timeout: 500,
|
|
168
|
+
force: true,
|
|
169
|
+
noWaitAfter: true
|
|
185
170
|
});
|
|
171
|
+
// Wait for any delayed and/or slowed hover reaction.
|
|
172
|
+
await page.waitForTimeout(200);
|
|
173
|
+
await root.waitForElementState('stable');
|
|
174
|
+
// Increment the counts of triggers and impacts.
|
|
175
|
+
const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
|
|
176
|
+
if (hasTimedOut) {
|
|
177
|
+
return Promise.resolve('');
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
data.totals.impactTriggers += popRatio;
|
|
181
|
+
data.totals.additions += popRatio * additionCount;
|
|
182
|
+
data.totals.removals += popRatio * removalCount;
|
|
183
|
+
data.totals.opacityChanges += popRatio * opacityChangers.length;
|
|
184
|
+
data.totals.opacityImpact += popRatio * opacityImpact;
|
|
185
|
+
// If details are to be reported:
|
|
186
|
+
if (withItems) {
|
|
187
|
+
// Report them.
|
|
188
|
+
data.items[region].impactTriggers.push({
|
|
189
|
+
tagName,
|
|
190
|
+
text: await textOf(firstTrigger, 50),
|
|
191
|
+
additions: additionCount,
|
|
192
|
+
removals: removalCount,
|
|
193
|
+
opacityChanges: opacityChangers.length,
|
|
194
|
+
opacityImpact
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (withItems) {
|
|
193
|
-
try {
|
|
194
|
-
const id = await firstTrigger.getAttribute('id');
|
|
195
|
-
data.items[region].unhoverables.push({
|
|
196
|
-
tagName,
|
|
197
|
-
id: id || '',
|
|
198
|
-
text: await textOf(firstTrigger, 50)
|
|
199
|
-
});
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
|
|
202
|
+
if (hasTimedOut) {
|
|
203
|
+
return Promise.resolve('');
|
|
200
204
|
}
|
|
201
|
-
|
|
202
|
-
|
|
205
|
+
else {
|
|
206
|
+
data.totals.unhoverables++;
|
|
207
|
+
if (withItems) {
|
|
208
|
+
try {
|
|
209
|
+
const id = await firstTrigger.getAttribute('id');
|
|
210
|
+
data.items[region].unhoverables.push({
|
|
211
|
+
tagName,
|
|
212
|
+
id: id || '',
|
|
213
|
+
text: await textOf(firstTrigger, 50)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch(error) {
|
|
217
|
+
console.log('ERROR itemizing unhoverable element');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
}
|
|
223
|
+
// Process the remaining potential triggers.
|
|
224
|
+
await find(data, withItems, page, region, sample.slice(1), popRatio);
|
|
206
225
|
}
|
|
207
|
-
|
|
208
|
-
|
|
226
|
+
catch(error) {
|
|
227
|
+
console.log(`ERROR: Test quit when remaining sample size was ${sample.length}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
return Promise.resolve('');
|
|
209
232
|
}
|
|
210
233
|
};
|
|
211
234
|
// Performs the hover test and reports results.
|
|
@@ -213,7 +236,7 @@ exports.reporter = async (
|
|
|
213
236
|
page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
|
|
214
237
|
) => {
|
|
215
238
|
// Initialize the result.
|
|
216
|
-
|
|
239
|
+
let data = {
|
|
217
240
|
totals: {
|
|
218
241
|
triggers: 0,
|
|
219
242
|
headTriggers: 0,
|
|
@@ -259,17 +282,34 @@ exports.reporter = async (
|
|
|
259
282
|
// Get the head and tail samples.
|
|
260
283
|
const headSample = getSample(headTriggers, headSampleSize);
|
|
261
284
|
const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
|
|
285
|
+
// Set a time limit to handle pages that slow the operations of this test.
|
|
286
|
+
const timeLimit = Math.round(1.3 * (headSample.length + tailSample.length));
|
|
287
|
+
const timeout = setTimeout(async () => {
|
|
288
|
+
await page.close();
|
|
289
|
+
console.log(
|
|
290
|
+
`ERROR: hover test timed out at ${timeLimit} seconds; page closed`
|
|
291
|
+
);
|
|
292
|
+
hasTimedOut = true;
|
|
293
|
+
data = {
|
|
294
|
+
prevented: true,
|
|
295
|
+
error: 'ERROR: hover test timed out'
|
|
296
|
+
};
|
|
297
|
+
clearTimeout(timeout);
|
|
298
|
+
}, 1000 * timeLimit);
|
|
262
299
|
// Find and document the impacts.
|
|
263
|
-
if (headSample.length) {
|
|
300
|
+
if (headSample.length && ! hasTimedOut) {
|
|
264
301
|
await find(data, withItems, page, 'head', headSample, headTriggerCount / headSample.length);
|
|
265
302
|
}
|
|
266
|
-
if (tailSample.length) {
|
|
303
|
+
if (tailSample.length && ! hasTimedOut) {
|
|
267
304
|
await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
|
|
268
305
|
}
|
|
306
|
+
clearTimeout(timeout);
|
|
269
307
|
// Round the reported totals.
|
|
270
|
-
|
|
271
|
-
data.totals
|
|
272
|
-
|
|
308
|
+
if (! hasTimedOut) {
|
|
309
|
+
Object.keys(data.totals).forEach(key => {
|
|
310
|
+
data.totals[key] = Math.round(data.totals[key]);
|
|
311
|
+
});
|
|
312
|
+
}
|
|
273
313
|
// Return the result.
|
|
274
314
|
return {result: data};
|
|
275
315
|
};
|