testaro 16.1.0 → 16.3.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/sample.js +30 -0
- package/procs/visChange.js +38 -8
- package/testaro/hovInd.js +297 -0
- package/testaro/hover.js +109 -387
- package/testaro/motion.js +7 -7
- package/tests/testaro.js +1 -0
- package/validation/tests/jobs/hovInd.json +179 -0
- package/validation/tests/jobs/hover.json +89 -143
- package/validation/tests/targets/hovInd/index.html +43 -0
- package/validation/tests/targets/hover/bad.html +10 -9
- package/testaro-old/allCaps.js +0 -94
- package/testaro-old/allSlanted.js +0 -86
- package/validation/tests/targets/hover/styleBad.html +0 -35
package/package.json
CHANGED
package/procs/sample.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
sample
|
|
3
|
+
Draws a decreasingly index-weighted random sample.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// FUNCTIONS
|
|
7
|
+
|
|
8
|
+
// Draws a location-weighted sample.
|
|
9
|
+
exports.getSample = (population, sampleSize) => {
|
|
10
|
+
const popSize = population.length;
|
|
11
|
+
// If the sample is smaller than the population:
|
|
12
|
+
if (sampleSize < popSize) {
|
|
13
|
+
// Assign to each trigger a priority randomly decreasing with its index.
|
|
14
|
+
const WeightedPopulation = population.map((item, index) => {
|
|
15
|
+
const weight = 1 + Math.sin(Math.PI * index / popSize + Math.PI / 2);
|
|
16
|
+
const priority = weight * Math.random();
|
|
17
|
+
return [index, priority];
|
|
18
|
+
});
|
|
19
|
+
// Return the population indexes of the items in the sample, in ascending order.
|
|
20
|
+
const sortedPopulation = WeightedPopulation.sort((a, b) => b[1] - a[1]);
|
|
21
|
+
const sample = sortedPopulation.slice(0, sampleSize);
|
|
22
|
+
const domOrderSample = sample.sort((a, b) => a[0] - b[0]);
|
|
23
|
+
return domOrderSample.map(trigger => trigger[0]);
|
|
24
|
+
}
|
|
25
|
+
// Otherwise, i.e. if the sample is at least as large as the population:
|
|
26
|
+
else {
|
|
27
|
+
// Return the population indexes.
|
|
28
|
+
return population.map((item, index) => index);
|
|
29
|
+
}
|
|
30
|
+
};
|
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
|
};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/*
|
|
2
|
+
hover
|
|
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
|
+
|
|
6
|
+
The elements that are subjected to hovering (called “triggers”) are the Playwright-visible
|
|
7
|
+
elements that have 'A', 'BUTTON', or (if not with role=menuitem) 'LI' tag names or have
|
|
8
|
+
'onmouseenter' or 'onmouseover' attributes.
|
|
9
|
+
|
|
10
|
+
Despite the delay, the test can make the execution time practical by randomly sampling triggers
|
|
11
|
+
instead of hovering over all of them. When sampling is performed, the results may vary from one
|
|
12
|
+
execution to another. Because hover impacts typically occur near the beginning of a page with
|
|
13
|
+
navigation menus, the probability of the inclusion of a trigger in a sample decreases with the
|
|
14
|
+
index of the trigger.
|
|
15
|
+
|
|
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.
|
|
26
|
+
|
|
27
|
+
WARNING: This test uses the Playwright page.screenshot method, which is not implemented for the
|
|
28
|
+
firefox browser type.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
// IMPORTS
|
|
32
|
+
|
|
33
|
+
// Module to get locator data.
|
|
34
|
+
const {getLocatorData} = require('../procs/getLocatorData');
|
|
35
|
+
// Module to draw a sample.
|
|
36
|
+
const {getSample} = require('../procs/sample');
|
|
37
|
+
|
|
38
|
+
// CONSTANTS
|
|
39
|
+
|
|
40
|
+
// Standard non-default hover cursors
|
|
41
|
+
const standardCursor = {
|
|
42
|
+
A: 'pointer',
|
|
43
|
+
INPUT: {
|
|
44
|
+
email: 'text',
|
|
45
|
+
image: 'pointer',
|
|
46
|
+
number: 'text',
|
|
47
|
+
password: 'text',
|
|
48
|
+
search: 'text',
|
|
49
|
+
tel: 'text',
|
|
50
|
+
text: 'text',
|
|
51
|
+
url: 'text'
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// FUNCTIONS
|
|
56
|
+
|
|
57
|
+
// Returns the hover-related style properties of a trigger.
|
|
58
|
+
const getHoverStyles = async loc => await loc.evaluate(element => {
|
|
59
|
+
const {
|
|
60
|
+
cursor,
|
|
61
|
+
borderColor,
|
|
62
|
+
borderStyle,
|
|
63
|
+
borderWidth,
|
|
64
|
+
outlineColor,
|
|
65
|
+
outlineStyle,
|
|
66
|
+
outlineWidth,
|
|
67
|
+
outlineOffset,
|
|
68
|
+
backgroundColor
|
|
69
|
+
} = window.getComputedStyle(element);
|
|
70
|
+
return {
|
|
71
|
+
tagName: element.tagName,
|
|
72
|
+
inputType: element.tagName === 'INPUT' ? element.getAttribute('type') || 'text' : null,
|
|
73
|
+
cursor: cursor.replace(/^.+, */, ''),
|
|
74
|
+
border: `${borderColor} ${borderStyle} ${borderWidth}`,
|
|
75
|
+
outline: `${outlineColor} ${outlineStyle} ${outlineWidth} ${outlineOffset}`,
|
|
76
|
+
backgroundColor
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
// Returns data on the hover cursor.
|
|
80
|
+
const getCursorData = hovStyles => {
|
|
81
|
+
const {cursor, tagName} = hovStyles;
|
|
82
|
+
const data = {
|
|
83
|
+
cursor
|
|
84
|
+
};
|
|
85
|
+
// If the element is an input or a link:
|
|
86
|
+
if (standardCursor[tagName]) {
|
|
87
|
+
// If it is an input:
|
|
88
|
+
if (tagName === 'INPUT') {
|
|
89
|
+
// Get whether its hover cursor is standard.
|
|
90
|
+
data.ok = cursor === (standardCursor.INPUT[hovStyles.inputType] || 'default');
|
|
91
|
+
}
|
|
92
|
+
// Otherwise, i.e. if it is a link:
|
|
93
|
+
else {
|
|
94
|
+
// Get whether its hover cursor is standard.
|
|
95
|
+
data.ok = cursor === 'pointer';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Otherwise, if it is a button:
|
|
99
|
+
else if (tagName === 'BUTTON') {
|
|
100
|
+
// Get whether its hover cursor is standard.
|
|
101
|
+
data.ok = cursor === 'default';
|
|
102
|
+
}
|
|
103
|
+
// Otherwise, i.e. if it has another type and a hover listener:
|
|
104
|
+
else {
|
|
105
|
+
// Assume its hover cursor is standard.
|
|
106
|
+
data.ok = true;
|
|
107
|
+
}
|
|
108
|
+
return data;
|
|
109
|
+
};
|
|
110
|
+
// Returns whether two hover styles are effectively identical.
|
|
111
|
+
const areAlike = (styles0, styles1) => {
|
|
112
|
+
// Return whether they are effectively identical.
|
|
113
|
+
const areAlike = ['outline', 'border', 'backgroundColor']
|
|
114
|
+
.every(style => styles1[style] === styles0[style]);
|
|
115
|
+
return areAlike;
|
|
116
|
+
};
|
|
117
|
+
// Performs the hovInd test and reports results.
|
|
118
|
+
exports.reporter = async (page, withItems, sampleSize = 20) => {
|
|
119
|
+
// Initialize the result.
|
|
120
|
+
const data = {
|
|
121
|
+
typeTotals: {
|
|
122
|
+
badCursor: 0,
|
|
123
|
+
hoverLikeDefault: 0,
|
|
124
|
+
hoverLikeFocus: 0
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const totals = [0, 0, 0, 0];
|
|
128
|
+
const standardInstances = [];
|
|
129
|
+
// Identify the triggers.
|
|
130
|
+
const selectors = ['a', 'button', 'input', '[onmouseenter]', '[onmouseover]'];
|
|
131
|
+
const selectorString = selectors.map(selector => `body ${selector}:visible`).join(', ');
|
|
132
|
+
const locAll = page.locator(selectorString);
|
|
133
|
+
const locsAll = await locAll.all();
|
|
134
|
+
// Get the population-to-sample ratio.
|
|
135
|
+
const psRatio = Math.max(1, locsAll.length / sampleSize);
|
|
136
|
+
// Get a sample of the triggers.
|
|
137
|
+
const sampleIndexes = getSample(locsAll, sampleSize);
|
|
138
|
+
const sample = locsAll.filter((loc, index) => sampleIndexes.includes(index));
|
|
139
|
+
// For each trigger in the sample:
|
|
140
|
+
for (const loc of sample) {
|
|
141
|
+
// Get its style properties.
|
|
142
|
+
const preStyles = await getHoverStyles(loc);
|
|
143
|
+
// Focus it.
|
|
144
|
+
await loc.focus();
|
|
145
|
+
// Get its style properties.
|
|
146
|
+
const focStyles = await getHoverStyles(loc);
|
|
147
|
+
// Hover over it.
|
|
148
|
+
await loc.hover();
|
|
149
|
+
// Get its style properties.
|
|
150
|
+
const fhStyles = await getHoverStyles(loc);
|
|
151
|
+
// Blur it.
|
|
152
|
+
await loc.blur({
|
|
153
|
+
timeout: 500
|
|
154
|
+
});
|
|
155
|
+
// Get its style properties.
|
|
156
|
+
const hovStyles = await getHoverStyles(loc);
|
|
157
|
+
// If all 4 style declarations belong to the same element:
|
|
158
|
+
if ([focStyles, fhStyles, hovStyles].every(style => style.code === preStyles.code)) {
|
|
159
|
+
// Get data on the element if itemization is required.
|
|
160
|
+
const elData = withItems ? await getLocatorData(loc) : null;
|
|
161
|
+
// If the hover cursor is nonstandard:
|
|
162
|
+
const cursorData = getCursorData(hovStyles);
|
|
163
|
+
if (! cursorData.ok) {
|
|
164
|
+
// Add to the totals.
|
|
165
|
+
totals[2] += psRatio;
|
|
166
|
+
data.typeTotals.badCursor += psRatio;
|
|
167
|
+
// If itemization is required:
|
|
168
|
+
if (withItems) {
|
|
169
|
+
// Add an instance to the result.
|
|
170
|
+
standardInstances.push({
|
|
171
|
+
ruleID: 'hovInd',
|
|
172
|
+
what: `Element has a nonstandard hover cursor (${cursorData.cursor})`,
|
|
173
|
+
ordinalSeverity: 2,
|
|
174
|
+
tagName: elData.tagName,
|
|
175
|
+
id: elData.id,
|
|
176
|
+
location: elData.location,
|
|
177
|
+
excerpt: elData.excerpt
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// If the element is a button and the hover and default states are not distinct:
|
|
182
|
+
if (hovStyles.tagName === 'BUTTON' && areAlike(preStyles, hovStyles)) {
|
|
183
|
+
// Add to the totals.
|
|
184
|
+
totals[1] += psRatio;
|
|
185
|
+
data.typeTotals.hoverLikeDefault += psRatio;
|
|
186
|
+
// If itemization is required:
|
|
187
|
+
if (withItems) {
|
|
188
|
+
// Add an instance to the result.
|
|
189
|
+
standardInstances.push({
|
|
190
|
+
ruleID: 'hovInd',
|
|
191
|
+
what: 'Element border, outline, and background color do not change when hovered over',
|
|
192
|
+
ordinalSeverity: 1,
|
|
193
|
+
tagName: elData.tagName,
|
|
194
|
+
id: elData.id,
|
|
195
|
+
location: elData.location,
|
|
196
|
+
excerpt: elData.excerpt
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// If the hover and focus-hover states are indistinct but differ from the default state:
|
|
201
|
+
if (areAlike(hovStyles, focStyles) && ! areAlike(hovStyles, preStyles)) {
|
|
202
|
+
// Add to the totals.
|
|
203
|
+
totals[1] += psRatio;
|
|
204
|
+
data.typeTotals.hoverLikeFocus += psRatio;
|
|
205
|
+
// If itemization is required:
|
|
206
|
+
if (withItems) {
|
|
207
|
+
// Add an instance to the result.
|
|
208
|
+
standardInstances.push({
|
|
209
|
+
ruleID: 'hovInd',
|
|
210
|
+
what: 'Element border, outline, and background color are alike on hover and focus',
|
|
211
|
+
ordinalSeverity: 1,
|
|
212
|
+
tagName: elData.tagName,
|
|
213
|
+
id: elData.id,
|
|
214
|
+
location: elData.location,
|
|
215
|
+
excerpt: elData.excerpt
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Otherwise, i.e. if the style properties do not all belong to the same element:
|
|
221
|
+
else {
|
|
222
|
+
// Report this.
|
|
223
|
+
data.prevented = true;
|
|
224
|
+
data.error = 'ERROR: Page changes on focus or hover prevent test';
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Round the totals.
|
|
228
|
+
Object.keys(data.typeTotals).forEach(rule => {
|
|
229
|
+
data.typeTotals[rule] = Math.round(data.typeTotals[rule]);
|
|
230
|
+
});
|
|
231
|
+
for (const index in totals) {
|
|
232
|
+
totals[index] = Math.round(totals[index]);
|
|
233
|
+
}
|
|
234
|
+
// If itemization is not required:
|
|
235
|
+
if (! withItems) {
|
|
236
|
+
// If any triggers have nonstandard hover cursors:
|
|
237
|
+
if (data.typeTotals.badCursor) {
|
|
238
|
+
// Add a summary instance to the result.
|
|
239
|
+
standardInstances.push({
|
|
240
|
+
ruleID: 'hovInd',
|
|
241
|
+
what: 'Elements have nonstandard hover cursors',
|
|
242
|
+
ordinalSeverity: 2,
|
|
243
|
+
count: data.typeTotals.badCursor,
|
|
244
|
+
tagName: '',
|
|
245
|
+
id: '',
|
|
246
|
+
location: {
|
|
247
|
+
doc: '',
|
|
248
|
+
type: '',
|
|
249
|
+
spec: ''
|
|
250
|
+
},
|
|
251
|
+
excerpt: ''
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// If any triggers have hover styles not distinct from their default styles:
|
|
255
|
+
if (data.typeTotals.hoverLikeDefault) {
|
|
256
|
+
// Add a summary instance to the result.
|
|
257
|
+
standardInstances.push({
|
|
258
|
+
ruleID: 'hovInd',
|
|
259
|
+
what: 'Element borders, outlines, and background colors do not change when hovered over',
|
|
260
|
+
ordinalSeverity: 1,
|
|
261
|
+
count: data.typeTotals.hoverLikeDefault,
|
|
262
|
+
tagName: '',
|
|
263
|
+
id: '',
|
|
264
|
+
location: {
|
|
265
|
+
doc: '',
|
|
266
|
+
type: '',
|
|
267
|
+
spec: ''
|
|
268
|
+
},
|
|
269
|
+
excerpt: ''
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
// If any triggers have focus-hover styles not distinct from their focus styles:
|
|
273
|
+
if (data.typeTotals.fhLikeFocus) {
|
|
274
|
+
// Add a summary instance to the result.
|
|
275
|
+
standardInstances.push({
|
|
276
|
+
ruleID: 'hovInd',
|
|
277
|
+
what: 'Element borders, outlines, and background colors on focus do not change when also hovered over',
|
|
278
|
+
ordinalSeverity: 1,
|
|
279
|
+
count: data.typeTotals.fhLikeFocus,
|
|
280
|
+
tagName: '',
|
|
281
|
+
id: '',
|
|
282
|
+
location: {
|
|
283
|
+
doc: '',
|
|
284
|
+
type: '',
|
|
285
|
+
spec: ''
|
|
286
|
+
},
|
|
287
|
+
excerpt: ''
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Return the result.
|
|
292
|
+
return {
|
|
293
|
+
data,
|
|
294
|
+
totals,
|
|
295
|
+
standardInstances
|
|
296
|
+
};
|
|
297
|
+
};
|