testaro 16.1.0 → 16.2.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/procs/visChange.js +38 -8
- package/testaro/hover.js +117 -374
- package/testaro/motion.js +7 -7
- package/validation/tests/jobs/hovInd.json +320 -0
- package/validation/tests/jobs/hover.json +89 -143
- package/validation/tests/targets/hover/bad.html +10 -9
- /package/validation/tests/targets/{hover → hovInd}/styleBad.html +0 -0
package/package.json
CHANGED
package/procs/visChange.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
visChange
|
|
3
|
-
This procedure reports a change in the visible content of a page between two times
|
|
3
|
+
This procedure reports a change in the visible content of a page between two times, optionally
|
|
4
|
+
hovering over a locator-defined element immediately after the first time.
|
|
4
5
|
WARNING: This test uses the Playwright page.screenshot method, which produces incorrect results
|
|
5
6
|
when the browser type is chromium and is not implemented for the firefox browser type. The only
|
|
6
7
|
browser type usable with this test is webkit.
|
|
@@ -17,15 +18,15 @@ const {PNG} = require('pngjs');
|
|
|
17
18
|
const shoot = async (page, exclusion = null) => {
|
|
18
19
|
// Make a screenshot as a buffer.
|
|
19
20
|
const options = {
|
|
20
|
-
fullPage:
|
|
21
|
+
fullPage: true,
|
|
21
22
|
omitBackground: true,
|
|
22
23
|
timeout: 2000
|
|
23
24
|
};
|
|
24
25
|
if (exclusion) {
|
|
26
|
+
const exclusionText = await exclusion.textContent();
|
|
25
27
|
options.mask = [exclusion];
|
|
26
28
|
}
|
|
27
|
-
return await page.screenshot(
|
|
28
|
-
})
|
|
29
|
+
return await page.screenshot(options)
|
|
29
30
|
.catch(error => {
|
|
30
31
|
console.log(`ERROR: Screenshot failed (${error.message})`);
|
|
31
32
|
return '';
|
|
@@ -37,10 +38,37 @@ exports.visChange = async (page, options = {}) => {
|
|
|
37
38
|
if (delayBefore) {
|
|
38
39
|
await page.waitForTimeout(delayBefore);
|
|
39
40
|
}
|
|
40
|
-
//
|
|
41
|
+
// If an exclusion was specified:
|
|
42
|
+
if (exclusion) {
|
|
43
|
+
// Hover over the upper-left corner of the page for test isolation.
|
|
44
|
+
const docLoc = page.locator('html');
|
|
45
|
+
await docLoc.hover({
|
|
46
|
+
position: {
|
|
47
|
+
x: 0,
|
|
48
|
+
y: 0
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Make a screenshot, excluding an element if specified.
|
|
41
53
|
const shot0 = await shoot(page, exclusion);
|
|
42
54
|
// If it succeeded:
|
|
43
55
|
if (shot0.length) {
|
|
56
|
+
// If an exclusion was specified:
|
|
57
|
+
if (exclusion) {
|
|
58
|
+
// Hover over it.
|
|
59
|
+
try {
|
|
60
|
+
await exclusion.hover({
|
|
61
|
+
timeout: 500,
|
|
62
|
+
noWaitAfter: true
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch(error) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: 'Hovering failed'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
44
72
|
// Wait as specified, or 3 seconds.
|
|
45
73
|
await page.waitForTimeout(delayBetween || 3000);
|
|
46
74
|
// Make another screenshot.
|
|
@@ -54,7 +82,7 @@ exports.visChange = async (page, options = {}) => {
|
|
|
54
82
|
// Get the count of differing pixels between the shots.
|
|
55
83
|
const pixelChanges = pixelmatch(pngs[0].data, pngs[1].data, null, width, height);
|
|
56
84
|
// Get the ratio of differing to all pixels as a percentage.
|
|
57
|
-
const changePercent =
|
|
85
|
+
const changePercent = 100 * pixelChanges / (width * height);
|
|
58
86
|
// Return this.
|
|
59
87
|
return {
|
|
60
88
|
success: true,
|
|
@@ -68,7 +96,8 @@ exports.visChange = async (page, options = {}) => {
|
|
|
68
96
|
else {
|
|
69
97
|
// Return this.
|
|
70
98
|
return {
|
|
71
|
-
success: false
|
|
99
|
+
success: false,
|
|
100
|
+
error: 'Second screenshot failed'
|
|
72
101
|
};
|
|
73
102
|
}
|
|
74
103
|
}
|
|
@@ -76,7 +105,8 @@ exports.visChange = async (page, options = {}) => {
|
|
|
76
105
|
else {
|
|
77
106
|
// Return this.
|
|
78
107
|
return {
|
|
79
|
-
success: false
|
|
108
|
+
success: false,
|
|
109
|
+
error: 'First screenshot failed'
|
|
80
110
|
};
|
|
81
111
|
}
|
|
82
112
|
};
|
package/testaro/hover.js
CHANGED
|
@@ -1,427 +1,170 @@
|
|
|
1
1
|
/*
|
|
2
2
|
hover
|
|
3
|
-
This test reports unexpected impacts of hovering
|
|
4
|
-
|
|
3
|
+
This test reports unexpected impacts of hovering on the visible page. Impacts are measured by
|
|
4
|
+
pixel changes outside the hovered element and by unhoverability.
|
|
5
|
+
|
|
5
6
|
The elements that are subjected to hovering (called “triggers”) are the Playwright-visible
|
|
6
7
|
elements that have 'A', 'BUTTON', or (if not with role=menuitem) 'LI' tag names or have
|
|
7
8
|
'onmouseenter' or 'onmouseover' attributes.
|
|
8
9
|
|
|
9
|
-
When a trigger is hovered over, the test examines the impacts on descendants of the great
|
|
10
|
-
grandparents of triggers with tag names 'A' and 'BUTTON', grandparents of triggers with tag
|
|
11
|
-
name 'LI', and otherwise the descendants of the triggers themselves. Four impacts are counted:
|
|
12
|
-
(1) an element is added or becomes visible, (2) an element is removed or becomes invisible, (3)
|
|
13
|
-
the opacity of an element changes, and (4) the element is a descendant of an element whose opacity
|
|
14
|
-
changes. The test checks up to 4 times for hovering impacts at intervals of 0.3 second.
|
|
15
|
-
|
|
16
10
|
Despite the delay, the test can make the execution time practical by randomly sampling triggers
|
|
17
11
|
instead of hovering over all of them. When sampling is performed, the results may vary from one
|
|
18
12
|
execution to another. Because hover impacts typically occur near the beginning of a page with
|
|
19
13
|
navigation menus, the probability of the inclusion of a trigger in a sample decreases with the
|
|
20
14
|
index of the trigger.
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
16
|
+
Pixel changes: If no pixel changes occur immediately after an element is hovered over, the page
|
|
17
|
+
is examined once more, after 0.5 second. The greater the fraction of changed pixels, the greater
|
|
18
|
+
the ordinal severity.
|
|
19
|
+
|
|
20
|
+
Unhoverability: An element is reported as unhoverable when it fails the Playwright actionability
|
|
21
|
+
checks for hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer
|
|
22
|
+
animating), and able to receive events. All triggers satisfy the first two conditions, so only the
|
|
23
|
+
last two might fail. Playwright defines the ability to receive events as being the target of an
|
|
24
|
+
action on the location where the center of the element is, rather than some other element with a
|
|
25
|
+
higher zIndex value in the same location being the target.
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
WARNING: This test uses the Playwright page.screenshot method, which is not implemented for the
|
|
28
|
+
firefox browser type.
|
|
30
29
|
*/
|
|
31
30
|
|
|
32
|
-
//
|
|
31
|
+
// IMPORTS
|
|
33
32
|
|
|
34
|
-
|
|
33
|
+
// Module to get locator data.
|
|
34
|
+
const {getLocatorData} = require('../procs/getLocatorData');
|
|
35
|
+
// Module to get pixel changes between two times.
|
|
36
|
+
const {visChange} = require('../procs/visChange');
|
|
35
37
|
|
|
36
38
|
// FUNCTIONS
|
|
37
39
|
|
|
38
40
|
// Draws a location-weighted sample of triggers.
|
|
39
41
|
const getSample = (population, sampleSize) => {
|
|
40
42
|
const popSize = population.length;
|
|
43
|
+
// If the sample is smaller than the population:
|
|
41
44
|
if (sampleSize < popSize) {
|
|
45
|
+
// Assign to each trigger a priority randomly decreasing with its index.
|
|
42
46
|
const WeightedPopulation = population.map((trigger, index) => {
|
|
43
47
|
const weight = 1 + Math.sin(Math.PI * index / popSize + Math.PI / 2);
|
|
44
48
|
const priority = weight * Math.random();
|
|
45
|
-
return [
|
|
49
|
+
return [index, priority];
|
|
46
50
|
});
|
|
47
|
-
|
|
51
|
+
// Return the indexes of the triggers with the highest priorities.
|
|
52
|
+
const sortedPopulation = WeightedPopulation.sort((a, b) => b[1] - a[1]);
|
|
48
53
|
const sample = sortedPopulation.slice(0, sampleSize);
|
|
49
|
-
const domOrderSample = sample.sort((a, b) => a[
|
|
54
|
+
const domOrderSample = sample.sort((a, b) => a[0] - b[0]);
|
|
50
55
|
return domOrderSample.map(trigger => trigger[0]);
|
|
51
56
|
}
|
|
57
|
+
// Otherwise, i.e. if the sample is at least as large as the population:
|
|
52
58
|
else {
|
|
53
|
-
|
|
59
|
+
// Return the population indexes.
|
|
60
|
+
return population.map((trigger, index) => index);
|
|
54
61
|
}
|
|
55
62
|
};
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
removals,
|
|
105
|
-
opacityChanges: opacityChangers.length,
|
|
106
|
-
opacityImpact
|
|
107
|
-
};
|
|
63
|
+
// Performs the hover test and reports results.
|
|
64
|
+
exports.reporter = async (page, withItems, sampleSize = 20) => {
|
|
65
|
+
// Initialize the result.
|
|
66
|
+
const data = {};
|
|
67
|
+
const totals = [0, 0, 0, 0];
|
|
68
|
+
const standardInstances = [];
|
|
69
|
+
// Identify the triggers.
|
|
70
|
+
const selectors = ['a', 'button', 'li:not([role=menuitem])', '[onmouseenter]', '[onmouseover]'];
|
|
71
|
+
const selectorString = selectors.map(selector => `body ${selector}:visible`).join(', ');
|
|
72
|
+
const locAll = page.locator(selectorString);
|
|
73
|
+
const locsAll = await locAll.all();
|
|
74
|
+
// Get the population-to-sample ratio.
|
|
75
|
+
const psRatio = Math.max(1, locsAll.length / sampleSize);
|
|
76
|
+
// Get a sample of the triggers.
|
|
77
|
+
const sampleIndexes = getSample(locsAll, sampleSize);
|
|
78
|
+
const sample = locsAll.filter((loc, index) => sampleIndexes.includes(index));
|
|
79
|
+
// For each trigger in the sample:
|
|
80
|
+
for (const loc of sample) {
|
|
81
|
+
// Hover over it and get the fractional pixel change.
|
|
82
|
+
const hoverData = await visChange(page, {
|
|
83
|
+
delayBefore: 0,
|
|
84
|
+
delayBetween: 500,
|
|
85
|
+
exclusion: loc
|
|
86
|
+
});
|
|
87
|
+
// If the hovering and measurement succeeded:
|
|
88
|
+
if (hoverData.success) {
|
|
89
|
+
// If any pixels changed:
|
|
90
|
+
if (hoverData.changePercent) {
|
|
91
|
+
// Get the ordinal severity from the fractional pixel change.
|
|
92
|
+
const ordinalSeverity = Math.floor(Math.min(3, 0.4 * Math.sqrt(hoverData.changePercent)));
|
|
93
|
+
// Add to the totals.
|
|
94
|
+
totals[ordinalSeverity] += psRatio;
|
|
95
|
+
// If itemization is required:
|
|
96
|
+
if (withItems) {
|
|
97
|
+
// Get data on the trigger.
|
|
98
|
+
const elData = await getLocatorData(loc);
|
|
99
|
+
// Add an instance to the result.
|
|
100
|
+
standardInstances.push({
|
|
101
|
+
ruleID: 'hover',
|
|
102
|
+
what: 'Hovering over the element changes the page',
|
|
103
|
+
ordinalSeverity,
|
|
104
|
+
tagName: elData.tagName,
|
|
105
|
+
id: elData.id,
|
|
106
|
+
location: elData.location,
|
|
107
|
+
excerpt: elData.excerpt
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
108
111
|
}
|
|
109
|
-
// Otherwise, i.e. if
|
|
112
|
+
// Otherwise, i.e. if hovering and measurement failed:
|
|
110
113
|
else {
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
// Add to the totals.
|
|
115
|
+
totals[3] += psRatio;
|
|
116
|
+
// If itemization is required:
|
|
117
|
+
if (withItems) {
|
|
118
|
+
// Get data on the trigger.
|
|
119
|
+
const elData = await getLocatorData(loc);
|
|
120
|
+
// Add an instance to the result.
|
|
121
|
+
standardInstances.push({
|
|
122
|
+
ruleID: 'hover',
|
|
123
|
+
what: 'Element is not hoverable',
|
|
124
|
+
ordinalSeverity: 3,
|
|
125
|
+
tagName: elData.tagName,
|
|
126
|
+
id: elData.id,
|
|
127
|
+
location: elData.location,
|
|
128
|
+
excerpt: elData.excerpt
|
|
129
|
+
});
|
|
130
|
+
}
|
|
117
131
|
}
|
|
118
|
-
|
|
119
|
-
// Otherwise, i.e. if the allowed trial count has been exhausted:
|
|
120
|
-
else {
|
|
121
|
-
// Report non-impact.
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
// Returns the hover-related style properties of a trigger.
|
|
126
|
-
const getHoverStyles = async (page, element) => await page.evaluate(
|
|
127
|
-
element => {
|
|
128
|
-
const {cursor, outline, color, backgroundColor} = window.getComputedStyle(element);
|
|
129
|
-
return {
|
|
130
|
-
cursor: cursor.replace(/^.+, */, ''),
|
|
131
|
-
outline,
|
|
132
|
-
color,
|
|
133
|
-
backgroundColor
|
|
134
|
-
};
|
|
135
|
-
}, element
|
|
136
|
-
);
|
|
137
|
-
// Recursively adds estimated and itemized impacts of hovering over triggers to data.
|
|
138
|
-
const find = async (data, withItems, page, sample) => {
|
|
139
|
-
// If any triggers remain and the test has not timed out:
|
|
140
|
-
if (sample.length && ! hasTimedOut) {
|
|
141
|
-
// Get and report the impacts until and unless the test times out.
|
|
132
|
+
// Reload the page to preserve locator integrity.
|
|
142
133
|
try {
|
|
143
|
-
|
|
144
|
-
const firstTrigger = sample[0];
|
|
145
|
-
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
146
|
-
.catch(() => '');
|
|
147
|
-
if (tagNameJSHandle) {
|
|
148
|
-
const tagName = await tagNameJSHandle.jsonValue();
|
|
149
|
-
// Identify the root of a subtree likely to contain impacted elements.
|
|
150
|
-
let root = firstTrigger;
|
|
151
|
-
if (['A', 'BUTTON', 'LI'].includes(tagName)) {
|
|
152
|
-
const rootJSHandle = await page.evaluateHandle(
|
|
153
|
-
trigger => {
|
|
154
|
-
const parent = trigger.parentElement || trigger;
|
|
155
|
-
const grandparent = parent.parentElement || parent;
|
|
156
|
-
const greatGrandparent = grandparent.parentElement || parent;
|
|
157
|
-
return trigger.tagName === 'LI' ? grandparent : greatGrandparent;
|
|
158
|
-
},
|
|
159
|
-
firstTrigger
|
|
160
|
-
);
|
|
161
|
-
root = rootJSHandle.asElement();
|
|
162
|
-
}
|
|
163
|
-
// Identify all the visible descendants of the root.
|
|
164
|
-
const preDescendants = await root.$$(':visible');
|
|
165
|
-
// Identify their opacities.
|
|
166
|
-
const preOpacities = await page.evaluate(elements => elements.map(
|
|
167
|
-
element => window.getComputedStyle(element).opacity
|
|
168
|
-
), preDescendants);
|
|
169
|
-
// Get the style properties of the trigger.
|
|
170
|
-
const triggerPreStyles = await getHoverStyles(page, firstTrigger);
|
|
171
|
-
const multiplier = data.sampling.triggers / data.sampling.triggerSample;
|
|
172
|
-
const itemData = {
|
|
173
|
-
tagName,
|
|
174
|
-
id: (await firstTrigger.getAttribute('id')) || '',
|
|
175
|
-
text: await textOf(firstTrigger, 100)
|
|
176
|
-
};
|
|
177
|
-
try {
|
|
178
|
-
// Hover over the trigger.
|
|
179
|
-
await firstTrigger.hover({
|
|
180
|
-
timeout: 500,
|
|
181
|
-
noWaitAfter: true
|
|
182
|
-
});
|
|
183
|
-
// Repeatedly seek impacts of the hover at intervals.
|
|
184
|
-
const impacts = await getImpacts(
|
|
185
|
-
300, 4, root, page, preDescendants, preOpacities, firstTrigger
|
|
186
|
-
);
|
|
187
|
-
// Get the style properties of the trigger.
|
|
188
|
-
const triggerPostStyles = await getHoverStyles(page, firstTrigger);
|
|
189
|
-
// Add cursor and other style defects to the data.
|
|
190
|
-
const cursor = triggerPreStyles.cursor;
|
|
191
|
-
// If the trigger has no cursor:
|
|
192
|
-
if (cursor === 'none') {
|
|
193
|
-
// Add this fact to the data.
|
|
194
|
-
data.totals.noCursors += multiplier;
|
|
195
|
-
if (withItems) {
|
|
196
|
-
data.items.noCursors.push(itemData);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// If the trigger has an improper cursor:
|
|
200
|
-
if (
|
|
201
|
-
tagName === 'A' && cursor !== 'pointer'
|
|
202
|
-
|| tagName === 'BUTTON' && cursor !== 'default'
|
|
203
|
-
){
|
|
204
|
-
// Add this fact to the data.
|
|
205
|
-
data.totals.badCursors += multiplier;
|
|
206
|
-
if (withItems) {
|
|
207
|
-
data.items.badCursors.push(itemData);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// If hover indication is illicit but is present:
|
|
211
|
-
if (
|
|
212
|
-
tagName === 'LI'
|
|
213
|
-
&& JSON.stringify(triggerPostStyles) !== JSON.stringify(triggerPreStyles)
|
|
214
|
-
) {
|
|
215
|
-
// Add this fact to the data.
|
|
216
|
-
data.totals.badIndicators += multiplier;
|
|
217
|
-
if (withItems) {
|
|
218
|
-
data.items.badIndicators.push(itemData);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// If there were any impacts:
|
|
222
|
-
if (impacts) {
|
|
223
|
-
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
224
|
-
await page.hover('body', {
|
|
225
|
-
position: {
|
|
226
|
-
x: 0,
|
|
227
|
-
y: 0
|
|
228
|
-
},
|
|
229
|
-
timeout: 500,
|
|
230
|
-
force: true,
|
|
231
|
-
noWaitAfter: true
|
|
232
|
-
});
|
|
233
|
-
// Wait for any delayed and/or slowed reaction.
|
|
234
|
-
await page.waitForTimeout(200);
|
|
235
|
-
await root.waitForElementState('stable');
|
|
236
|
-
// Increment the estimated counts of triggers and impacts.
|
|
237
|
-
const {additions, removals, opacityChanges, opacityImpact} = impacts;
|
|
238
|
-
if (hasTimedOut) {
|
|
239
|
-
return Promise.resolve('');
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
data.totals.impactTriggers += multiplier;
|
|
243
|
-
data.totals.additions += additions * multiplier;
|
|
244
|
-
data.totals.removals += removals * multiplier;
|
|
245
|
-
data.totals.opacityChanges += opacityChanges * multiplier;
|
|
246
|
-
data.totals.opacityImpact += opacityImpact * multiplier;
|
|
247
|
-
// If details are to be reported:
|
|
248
|
-
if (withItems) {
|
|
249
|
-
// Add them to the data.
|
|
250
|
-
data.items.impactTriggers.push({
|
|
251
|
-
tagName,
|
|
252
|
-
id: itemData.id,
|
|
253
|
-
text: await textOf(firstTrigger, 100),
|
|
254
|
-
additions,
|
|
255
|
-
removals,
|
|
256
|
-
opacityChanges,
|
|
257
|
-
opacityImpact
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
catch (error) {
|
|
264
|
-
console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
|
|
265
|
-
if (hasTimedOut) {
|
|
266
|
-
return Promise.resolve('');
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
data.totals.unhoverables += multiplier;
|
|
270
|
-
if (withItems) {
|
|
271
|
-
try {
|
|
272
|
-
data.items.unhoverables.push(itemData);
|
|
273
|
-
}
|
|
274
|
-
catch(error) {
|
|
275
|
-
console.log('ERROR itemizing unhoverable element');
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
// Process the remaining potential triggers.
|
|
282
|
-
await find(data, withItems, page, sample.slice(1));
|
|
134
|
+
await page.reload({timeout: 5000});
|
|
283
135
|
}
|
|
284
136
|
catch(error) {
|
|
285
|
-
console.log(
|
|
137
|
+
console.log('ERROR: page reload timed out');
|
|
286
138
|
}
|
|
287
139
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
let data = {
|
|
296
|
-
sampling: {
|
|
297
|
-
triggers: 0,
|
|
298
|
-
triggerSample: 0,
|
|
299
|
-
},
|
|
300
|
-
totals: {
|
|
301
|
-
impactTriggers: 0,
|
|
302
|
-
additions: 0,
|
|
303
|
-
removals: 0,
|
|
304
|
-
opacityChanges: 0,
|
|
305
|
-
opacityImpact: 0,
|
|
306
|
-
unhoverables: 0,
|
|
307
|
-
noCursors: 0,
|
|
308
|
-
badCursors: 0,
|
|
309
|
-
badIndicators: 0
|
|
310
|
-
}
|
|
311
|
-
};
|
|
312
|
-
// If details are to be reported:
|
|
313
|
-
if (withItems) {
|
|
314
|
-
// Add properties for details to the initialized result.
|
|
315
|
-
data.items = {
|
|
316
|
-
impactTriggers: [],
|
|
317
|
-
unhoverables: [],
|
|
318
|
-
noCursors: [],
|
|
319
|
-
badCursors: [],
|
|
320
|
-
badIndicators: []
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
// Identify the triggers.
|
|
324
|
-
const selectors = ['a', 'button', 'li:not([role=menuitem])', '[onmouseenter]', '[onmouseover]'];
|
|
325
|
-
const triggers = await page.$$(selectors.map(selector => `body ${selector}:visible`).join(', '))
|
|
326
|
-
.catch(error => {
|
|
327
|
-
console.log(`ERROR getting hover triggers (${error.message})`);
|
|
328
|
-
data.prevented = true;
|
|
329
|
-
return [];
|
|
330
|
-
});
|
|
331
|
-
data.sampling.triggers = triggers.length;
|
|
332
|
-
// Get the sample.
|
|
333
|
-
const sample = getSample(triggers, sampleSize);
|
|
334
|
-
data.sampling.triggerSample = sample.length;
|
|
335
|
-
// Set a time limit to cover possible 2 seconds per trigger.
|
|
336
|
-
const timeLimit = Math.round(2.8 * sample.length + 2);
|
|
337
|
-
const timeout = setTimeout(async () => {
|
|
338
|
-
await page.close();
|
|
339
|
-
console.log(
|
|
340
|
-
`ERROR: hover test on sample of ${sample.length} triggers timed out at ${timeLimit} seconds; page closed`
|
|
341
|
-
);
|
|
342
|
-
hasTimedOut = true;
|
|
343
|
-
data = {
|
|
344
|
-
prevented: true,
|
|
345
|
-
error: 'ERROR: hover test timed out'
|
|
346
|
-
};
|
|
347
|
-
clearTimeout(timeout);
|
|
348
|
-
}, 1000 * timeLimit);
|
|
349
|
-
// Find and document the style defects and impacts of the sampled triggers.
|
|
350
|
-
if (sample.length && ! hasTimedOut) {
|
|
351
|
-
await find(data, withItems, page, sample);
|
|
352
|
-
}
|
|
353
|
-
clearTimeout(timeout);
|
|
354
|
-
// Round the reported totals.
|
|
355
|
-
if (! hasTimedOut) {
|
|
356
|
-
Object.keys(data.totals).forEach(key => {
|
|
357
|
-
data.totals[key] = Math.round(data.totals[key]);
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
const severity = {
|
|
361
|
-
impactTriggers: 3,
|
|
362
|
-
additions: 1,
|
|
363
|
-
removals: 2,
|
|
364
|
-
opacityChanges: 1,
|
|
365
|
-
opacityImpact: 0,
|
|
366
|
-
unhoverables: 3,
|
|
367
|
-
noCursors: 3,
|
|
368
|
-
badCursors: 2,
|
|
369
|
-
badIndicators: 2
|
|
370
|
-
};
|
|
371
|
-
const what = {
|
|
372
|
-
impactTriggers: 'Hovering over the element has unexpected effects',
|
|
373
|
-
unhoverables: 'Operable element cannot be hovered over',
|
|
374
|
-
noCursors: 'Hoverable element hides the mouse cursor',
|
|
375
|
-
badCursors: 'Link or button makes the hovering mouse cursor nonstandard',
|
|
376
|
-
badIndicators: 'List item changes when hovered over'
|
|
377
|
-
};
|
|
378
|
-
const totals = [0, 0, 0, 0];
|
|
379
|
-
Object.keys(data.totals).forEach(category => {
|
|
380
|
-
totals[severity[category]] += data.totals[category];
|
|
381
|
-
});
|
|
382
|
-
const standardInstances = [];
|
|
383
|
-
if (data.items) {
|
|
384
|
-
Object.keys(data.items).forEach(category => {
|
|
385
|
-
data.items[category].forEach(item => {
|
|
140
|
+
// If itemization is not required:
|
|
141
|
+
if (! withItems) {
|
|
142
|
+
// For each ordinal severity:
|
|
143
|
+
for (const index in totals) {
|
|
144
|
+
// If there were any instances with it:
|
|
145
|
+
if (totals[index]) {
|
|
146
|
+
// Add a summary instance to the result.
|
|
386
147
|
standardInstances.push({
|
|
387
148
|
ruleID: 'hover',
|
|
388
|
-
what:
|
|
389
|
-
ordinalSeverity:
|
|
390
|
-
|
|
391
|
-
|
|
149
|
+
what: 'Hovering over elements changes the page or fails',
|
|
150
|
+
ordinalSeverity: index,
|
|
151
|
+
count: Math.round(totals[index]),
|
|
152
|
+
tagName: '',
|
|
153
|
+
id: '',
|
|
392
154
|
location: {
|
|
393
155
|
doc: '',
|
|
394
156
|
type: '',
|
|
395
157
|
spec: ''
|
|
396
158
|
},
|
|
397
|
-
excerpt:
|
|
159
|
+
excerpt: ''
|
|
398
160
|
});
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
else if (totals.some(total => total)) {
|
|
403
|
-
standardInstances.push({
|
|
404
|
-
ruleID: 'hover',
|
|
405
|
-
what: 'Hovering behaves unexpectedly',
|
|
406
|
-
ordinalSeverity: totals.reduce((max, current, index) => current ? index : max, 0),
|
|
407
|
-
count: Object.values(data.totals).reduce((total, current) => total + current),
|
|
408
|
-
tagName: '',
|
|
409
|
-
id: '',
|
|
410
|
-
location: {
|
|
411
|
-
doc: '',
|
|
412
|
-
type: '',
|
|
413
|
-
spec: ''
|
|
414
|
-
},
|
|
415
|
-
excerpt: ''
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
// Reload the page.
|
|
419
|
-
try {
|
|
420
|
-
await page.reload({timeout: 15000});
|
|
421
|
-
}
|
|
422
|
-
catch(error) {
|
|
423
|
-
console.log('ERROR: page reload timed out');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
424
163
|
}
|
|
164
|
+
// Round the totals.
|
|
165
|
+
totals.forEach((total, index) => {
|
|
166
|
+
totals[index] = Math.round(totals[index]);
|
|
167
|
+
});
|
|
425
168
|
// Return the result.
|
|
426
169
|
return {
|
|
427
170
|
data,
|