testaro 4.13.0 → 4.15.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 +5 -1
- package/package.json +1 -1
- package/run.js +5 -2
- package/tests/aatt.js +1 -1
- package/tests/hover.js +175 -148
- package/tests/ibm.js +2 -2
package/commands.js
CHANGED
|
@@ -180,7 +180,11 @@ exports.commands = {
|
|
|
180
180
|
hover: [
|
|
181
181
|
'Perform a hover test',
|
|
182
182
|
{
|
|
183
|
-
|
|
183
|
+
headSize: [false, 'number', '', 'count of first triggers to sample separately, if any'],
|
|
184
|
+
headSampleSize: [false, 'number', '', 'size of the head sample to be drawn, if any'],
|
|
185
|
+
tailSampleSize: [
|
|
186
|
+
false, 'number', '', 'size of the non-head sample to be drawn, if not all'
|
|
187
|
+
],
|
|
184
188
|
withItems: [true, 'boolean']
|
|
185
189
|
}
|
|
186
190
|
],
|
package/package.json
CHANGED
package/run.js
CHANGED
|
@@ -289,9 +289,12 @@ const launch = async typeName => {
|
|
|
289
289
|
browserContext = await browser.newContext(viewport);
|
|
290
290
|
// When a page is added to the browser context:
|
|
291
291
|
browserContext.on('page', page => {
|
|
292
|
-
// Make its console messages get reported
|
|
292
|
+
// Make abbreviations of its console messages get reported in the Playwright console.
|
|
293
293
|
page.on('console', msg => {
|
|
294
|
-
|
|
294
|
+
let msgText = msg.text();
|
|
295
|
+
if (msgText.length > 300) {
|
|
296
|
+
msgText = `${msgText.slice(0, 150)} ... ${msgText.slice(-150)}`;
|
|
297
|
+
}
|
|
295
298
|
console.log(`[${msgText}]`);
|
|
296
299
|
const msgTextLC = msgText.toLowerCase();
|
|
297
300
|
const msgLength = msgText.length;
|
package/tests/aatt.js
CHANGED
package/tests/hover.js
CHANGED
|
@@ -1,63 +1,71 @@
|
|
|
1
1
|
/*
|
|
2
2
|
hover
|
|
3
|
-
This test reports unexpected
|
|
4
|
-
visible
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
This test reports unexpected impacts of hovering. The effects include additions and removals
|
|
4
|
+
of visible elements, opacity changes, and unhoverable elements. The elements that are
|
|
5
|
+
subjected to hovering (called “triggers”) are the Playwright-visible elements that have 'A',
|
|
6
|
+
'BUTTON', or 'LI' tag names or have 'onmouseenter' or 'onmouseover' attributes. When such an
|
|
7
|
+
element is hovered over, the test examines the impacts on descendants of the great grandparents
|
|
8
|
+
of the elements with tag names 'A' and 'BUTTON', grandparents of elements with tag name 'LI',
|
|
9
|
+
and otherwise the descendants of the elements themselves. Four impacts are counted: (1) an
|
|
10
|
+
element is added or becomes visible, (2) an element is removed or becomes invisible, (3) the
|
|
11
|
+
opacity of an element changes, and (4) the element is a descendant of an element whose opacity
|
|
12
|
+
changes. The test checks up to 4 times for hovering impacts at intervals of 0.3 second.
|
|
13
|
+
|
|
14
|
+
Despite this delay, the test can make the execution time practical by randomly sampling triggers
|
|
15
|
+
instead of hovering over all of them. When sampling is performed, the results may vary from one
|
|
16
|
+
execution to another. Because hover impacts typically occur near the beginning of a page,
|
|
17
|
+
sampling is governed by three optional parameters (defaults in parentheses):
|
|
18
|
+
headSize (0): the size of an initial subset of triggers (“head”)
|
|
19
|
+
headSampleSize (-1): the size of the sample to be drawn from the head
|
|
20
|
+
tailSampleSize (-1): the size of the sample to be drawn from the remainder of the page
|
|
21
|
+
A sample size of -1 means that there is no sampling, and the entire population is tested.
|
|
22
|
+
|
|
23
|
+
An element is reported as unhoverable when it fails the Playwright actionability checks for
|
|
24
|
+
hovering, i.e. fails to be attached to the DOM, visible, stable (not or no longer animating), and
|
|
25
|
+
able to receive events. All triggers satisfy the first two conditions, so only the last two might
|
|
26
|
+
fail. Playwright defines the ability to receive events as being the target of an action on the
|
|
27
|
+
location where the center of the element is, rather than some other element with a higher zIndex
|
|
28
|
+
value in the same location being the target.
|
|
21
29
|
*/
|
|
22
30
|
|
|
23
31
|
// CONSTANTS
|
|
24
32
|
|
|
25
|
-
// Selectors of active elements likely to be disclosed by a hover.
|
|
26
|
-
const targetSelectors = ['a', 'button', 'input', '[role=menuitem]', 'span']
|
|
27
|
-
.map(selector => `${selector}:visible`)
|
|
28
|
-
.join(', ');
|
|
29
33
|
// Initialize the result.
|
|
30
34
|
const data = {
|
|
31
|
-
populationSize: 0,
|
|
32
35
|
totals: {
|
|
33
36
|
triggers: 0,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
headTriggers: 0,
|
|
38
|
+
tailTriggers: 0,
|
|
39
|
+
impactTriggers: 0,
|
|
40
|
+
additions: 0,
|
|
41
|
+
removals: 0,
|
|
42
|
+
opacityChanges: 0,
|
|
43
|
+
opacityImpact: 0,
|
|
37
44
|
unhoverables: 0
|
|
38
45
|
}
|
|
39
46
|
};
|
|
40
47
|
|
|
41
|
-
// VARIABLES
|
|
42
|
-
|
|
43
|
-
// Counter.
|
|
44
|
-
let elementsChecked = 0;
|
|
45
|
-
|
|
46
48
|
// FUNCTIONS
|
|
47
49
|
|
|
48
50
|
// Samples a population and returns the sample.
|
|
49
51
|
const getSample = (population, sampleSize) => {
|
|
50
52
|
const popSize = population.length;
|
|
51
|
-
if (sampleSize
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (sampleSize === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
else if (sampleSize > 0 && sampleSize < popSize) {
|
|
57
|
+
const popData = [];
|
|
58
|
+
for (const trigger of population) {
|
|
59
|
+
popData.push({
|
|
60
|
+
trigger,
|
|
61
|
+
sorter: Math.random()
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
popData.sort((a, b) => a.sorter - b.sorter);
|
|
65
|
+
return popData.slice(0, sampleSize).map(obj => obj.trigger);
|
|
58
66
|
}
|
|
59
67
|
else {
|
|
60
|
-
return
|
|
68
|
+
return population;
|
|
61
69
|
}
|
|
62
70
|
};
|
|
63
71
|
// Returns the text of an element.
|
|
@@ -66,12 +74,12 @@ const textOf = async (element, limit) => {
|
|
|
66
74
|
text = text.trim() || await element.innerHTML();
|
|
67
75
|
return text.trim().replace(/\s*/sg, '').slice(0, limit);
|
|
68
76
|
};
|
|
69
|
-
// Recursively
|
|
70
|
-
const find = async (withItems, page,
|
|
71
|
-
// If any potential
|
|
72
|
-
if (
|
|
77
|
+
// Recursively reports impacts of hovering over triggers.
|
|
78
|
+
const find = async (withItems, page, region, sample, popRatio) => {
|
|
79
|
+
// If any potential triggers remain:
|
|
80
|
+
if (sample.length) {
|
|
73
81
|
// Identify the first of them.
|
|
74
|
-
const firstTrigger =
|
|
82
|
+
const firstTrigger = sample[0];
|
|
75
83
|
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
76
84
|
.catch(error => {
|
|
77
85
|
console.log(`ERROR getting trigger tag name (${error.message})`);
|
|
@@ -79,59 +87,86 @@ const find = async (withItems, page, triggers) => {
|
|
|
79
87
|
});
|
|
80
88
|
if (tagNameJSHandle) {
|
|
81
89
|
const tagName = await tagNameJSHandle.jsonValue();
|
|
82
|
-
// Identify the root of a subtree likely to contain
|
|
90
|
+
// Identify the root of a subtree likely to contain impacted elements.
|
|
83
91
|
let root = firstTrigger;
|
|
84
|
-
if (['A', 'BUTTON'].includes(tagName)) {
|
|
92
|
+
if (['A', 'BUTTON', 'LI'].includes(tagName)) {
|
|
85
93
|
const rootJSHandle = await page.evaluateHandle(
|
|
86
94
|
firstTrigger => {
|
|
87
95
|
const parent = firstTrigger.parentElement || firstTrigger;
|
|
88
96
|
const grandparent = parent.parentElement || parent;
|
|
89
|
-
|
|
97
|
+
const greatGrandparent = grandparent.parentElement || parent;
|
|
98
|
+
return firstTrigger.tagName === 'LI' ? grandparent : greatGrandparent;
|
|
90
99
|
},
|
|
91
100
|
firstTrigger
|
|
92
101
|
);
|
|
93
102
|
root = rootJSHandle.asElement();
|
|
94
103
|
}
|
|
95
|
-
// Identify the visible active descendants of the root before the hover.
|
|
96
|
-
const preVisibles = await root.$$(targetSelectors);
|
|
97
104
|
// Identify all the descendants of the root.
|
|
98
|
-
const
|
|
99
|
-
// Identify their opacities
|
|
100
|
-
const preOpacities = await page.evaluate(
|
|
101
|
-
|
|
102
|
-
);
|
|
105
|
+
const preDescendants = await root.$$('*');
|
|
106
|
+
// Identify their opacities.
|
|
107
|
+
const preOpacities = await page.evaluate(elements => elements.map(
|
|
108
|
+
element => window.getComputedStyle(element).opacity
|
|
109
|
+
), preDescendants);
|
|
103
110
|
try {
|
|
104
|
-
// Hover over the
|
|
105
|
-
await firstTrigger.hover({
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
111
|
+
// Hover over the trigger.
|
|
112
|
+
await firstTrigger.hover({
|
|
113
|
+
timeout: 500,
|
|
114
|
+
noWaitAfter: true
|
|
115
|
+
});
|
|
116
|
+
// Repeatedly seeks impacts.
|
|
117
|
+
const getImpacts = async (interval, triesLeft) => {
|
|
118
|
+
if (triesLeft--) {
|
|
119
|
+
const postDescendants = await root.$$('*');
|
|
120
|
+
const remainerIndexes = await page.evaluate(args => {
|
|
121
|
+
const preDescendants = args[0];
|
|
122
|
+
const postDescendants = args[1];
|
|
123
|
+
const remainerIndexes = preDescendants
|
|
124
|
+
.map((element, index) => postDescendants.includes(element) ? index : -1)
|
|
125
|
+
.filter(index => index > -1);
|
|
126
|
+
return remainerIndexes;
|
|
127
|
+
}, [preDescendants, postDescendants]);
|
|
128
|
+
const additionCount = postDescendants.length - remainerIndexes.length;
|
|
129
|
+
const removalCount = preDescendants.length - remainerIndexes.length;
|
|
130
|
+
const remainers = [];
|
|
131
|
+
for (const index of remainerIndexes) {
|
|
132
|
+
remainers.push({
|
|
133
|
+
element: preDescendants[index],
|
|
134
|
+
preOpacity: preOpacities[index],
|
|
135
|
+
postOpacity: await page.evaluate(
|
|
136
|
+
element => window.getComputedStyle(element).opacity, preDescendants[index]
|
|
137
|
+
)
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const opacityChangers = remainers
|
|
141
|
+
.filter(remainer => remainer.postOpacity !== remainer.preOpacity);
|
|
142
|
+
const opacityImpact = opacityChangers ? await page.evaluate(changers => changers.reduce(
|
|
143
|
+
(total, current) => total + current.element.querySelectorAll('*').length, 0
|
|
144
|
+
), opacityChangers) : 0;
|
|
145
|
+
if (additionCount || removalCount || opacityChangers.length) {
|
|
146
|
+
return {
|
|
147
|
+
additionCount,
|
|
148
|
+
removalCount,
|
|
149
|
+
opacityChangers,
|
|
150
|
+
opacityImpact
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
return await new Promise(resolve => {
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
resolve(getImpacts(interval, triesLeft));
|
|
157
|
+
}, interval);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
return null;
|
|
133
163
|
}
|
|
134
|
-
|
|
164
|
+
};
|
|
165
|
+
// Repeatedly seek impacts of the hover at intervals.
|
|
166
|
+
const impacts = await getImpacts(300, 4);
|
|
167
|
+
// If there were any:
|
|
168
|
+
if (impacts) {
|
|
169
|
+
// Hover over the upper-left corner of the page, to undo any impacts.
|
|
135
170
|
await page.hover('body', {
|
|
136
171
|
position: {
|
|
137
172
|
x: 0,
|
|
@@ -141,100 +176,92 @@ const find = async (withItems, page, triggers) => {
|
|
|
141
176
|
// Wait for any delayed and/or slowed hover reaction.
|
|
142
177
|
await page.waitForTimeout(200);
|
|
143
178
|
await root.waitForElementState('stable');
|
|
144
|
-
// Increment the counts of triggers and
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
data.totals.
|
|
148
|
-
data.totals.
|
|
149
|
-
data.totals.
|
|
179
|
+
// Increment the counts of triggers and impacts.
|
|
180
|
+
const {additionCount, removalCount, opacityChangers, opacityImpact} = impacts;
|
|
181
|
+
data.totals.impactTriggers += popRatio;
|
|
182
|
+
data.totals.additions += popRatio * additionCount;
|
|
183
|
+
data.totals.removals += popRatio * removalCount;
|
|
184
|
+
data.totals.opacityChanges += popRatio * opacityChangers.length;
|
|
185
|
+
data.totals.opacityImpact += popRatio * opacityImpact;
|
|
150
186
|
// If details are to be reported:
|
|
151
187
|
if (withItems) {
|
|
152
188
|
// Report them.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const postVisibles = args[2];
|
|
162
|
-
const madeVisible = postVisibles
|
|
163
|
-
.filter(el => ! preVisibles.includes(el))
|
|
164
|
-
.map(el => ({
|
|
165
|
-
tagName: el.tagName,
|
|
166
|
-
text: textOf(el, 50)
|
|
167
|
-
}));
|
|
168
|
-
const opacityChanged = args[3].map(el => ({
|
|
169
|
-
tagName: el.tagName,
|
|
170
|
-
text: textOf(el, 50)
|
|
171
|
-
}));
|
|
172
|
-
return {
|
|
173
|
-
tagName: trigger.tagName,
|
|
174
|
-
id: trigger.id || '',
|
|
175
|
-
text: textOf(trigger, 50),
|
|
176
|
-
madeVisible,
|
|
177
|
-
opacityChanged
|
|
178
|
-
};
|
|
179
|
-
}, [firstTrigger, preVisibles, postVisibles, opacityTargets]);
|
|
180
|
-
const triggerData = await triggerDataJSHandle.jsonValue();
|
|
181
|
-
data.items.triggers.push(triggerData);
|
|
189
|
+
data.items[region].impactTriggers.push({
|
|
190
|
+
tagName,
|
|
191
|
+
text: await textOf(firstTrigger, 50),
|
|
192
|
+
additions: additionCount,
|
|
193
|
+
removals: removalCount,
|
|
194
|
+
opacityChanges: opacityChangers.length,
|
|
195
|
+
opacityImpact
|
|
196
|
+
});
|
|
182
197
|
}
|
|
183
198
|
}
|
|
184
199
|
}
|
|
185
200
|
catch (error) {
|
|
186
|
-
console.log(
|
|
201
|
+
console.log(`ERROR hovering (${error.message})`);
|
|
187
202
|
data.totals.unhoverables++;
|
|
188
203
|
if (withItems) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
204
|
+
const id = await firstTrigger.getAttribute('id');
|
|
205
|
+
data.items[region].unhoverables.push({
|
|
206
|
+
tagName,
|
|
207
|
+
id: id || '',
|
|
192
208
|
text: await textOf(firstTrigger, 50)
|
|
193
209
|
});
|
|
194
210
|
}
|
|
195
211
|
}
|
|
196
212
|
}
|
|
197
213
|
// Process the remaining potential triggers.
|
|
198
|
-
await find(withItems, page,
|
|
214
|
+
await find(withItems, page, region, sample.slice(1), popRatio);
|
|
199
215
|
}
|
|
200
216
|
};
|
|
201
|
-
// Performs hover test and reports results.
|
|
202
|
-
exports.reporter = async (
|
|
217
|
+
// Performs the hover test and reports results.
|
|
218
|
+
exports.reporter = async (
|
|
219
|
+
page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
|
|
220
|
+
) => {
|
|
203
221
|
// If details are to be reported:
|
|
204
222
|
if (withItems) {
|
|
205
223
|
// Add properties for details to the initialized result.
|
|
206
224
|
data.items = {
|
|
207
|
-
|
|
208
|
-
|
|
225
|
+
head: {
|
|
226
|
+
impactTriggers: [],
|
|
227
|
+
unhoverables: []
|
|
228
|
+
},
|
|
229
|
+
tail: {
|
|
230
|
+
impactTriggers: [],
|
|
231
|
+
unhoverables: []
|
|
232
|
+
}
|
|
209
233
|
};
|
|
210
234
|
}
|
|
211
235
|
// Identify the triggers.
|
|
212
|
-
const selectors = [
|
|
213
|
-
|
|
214
|
-
'body button:visible',
|
|
215
|
-
'body li:visible, body [onmouseenter]:visible',
|
|
216
|
-
'body [onmouseover]:visible'
|
|
217
|
-
];
|
|
218
|
-
const triggers = await page.$$(selectors.join(', '))
|
|
236
|
+
const selectors = ['a', 'button', 'li', '[onmouseenter]', '[onmouseover]'];
|
|
237
|
+
const triggers = await page.$$(selectors.map(selector => `body ${selector}:visible`).join(', '))
|
|
219
238
|
.catch(error => {
|
|
220
239
|
console.log(`ERROR getting hover triggers (${error.message})`);
|
|
221
240
|
data.prevented = true;
|
|
222
241
|
return [];
|
|
223
242
|
});
|
|
224
|
-
//
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
243
|
+
// Classify them into head and tail triggers.
|
|
244
|
+
const headTriggers = triggers.slice(0, headSize);
|
|
245
|
+
const tailTriggers = triggers.slice(headSize);
|
|
246
|
+
const headTriggerCount = headTriggers.length;
|
|
247
|
+
const tailTriggerCount = tailTriggers.length;
|
|
248
|
+
data.totals.triggers = headTriggerCount + tailTriggerCount;
|
|
249
|
+
data.totals.headTriggers = headTriggerCount;
|
|
250
|
+
data.totals.tailTriggers = tailTriggerCount;
|
|
251
|
+
// Get the head and tail samples.
|
|
252
|
+
const headSample = getSample(headTriggers, headSampleSize);
|
|
253
|
+
const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
|
|
254
|
+
// Find and document the impacts.
|
|
255
|
+
if (headSample.length) {
|
|
256
|
+
await find(withItems, page, 'head', headSample, headTriggerCount / headSample.length);
|
|
257
|
+
}
|
|
258
|
+
if (tailSample.length) {
|
|
259
|
+
await find(withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
|
|
237
260
|
}
|
|
261
|
+
// Round the reported totals.
|
|
262
|
+
Object.keys(data.totals).forEach(key => {
|
|
263
|
+
data.totals[key] = Math.round(data.totals[key]);
|
|
264
|
+
});
|
|
238
265
|
// Return the result.
|
|
239
266
|
return {result: data};
|
|
240
267
|
};
|
package/tests/ibm.js
CHANGED
|
@@ -25,7 +25,6 @@ const run = async content => {
|
|
|
25
25
|
const nowLabel = (new Date()).toISOString().slice(0, 19);
|
|
26
26
|
// Return the result of a test.
|
|
27
27
|
const ibmReport = await getCompliance(content, nowLabel);
|
|
28
|
-
await close();
|
|
29
28
|
return ibmReport;
|
|
30
29
|
};
|
|
31
30
|
// Trims an IBM report.
|
|
@@ -100,7 +99,7 @@ exports.reporter = async (page, withItems, withNewContent) => {
|
|
|
100
99
|
// If a test with existing content is to be performed:
|
|
101
100
|
const result = {};
|
|
102
101
|
if (! withNewContent) {
|
|
103
|
-
const timeLimit =
|
|
102
|
+
const timeLimit = 20;
|
|
104
103
|
const typeContent = await page.content();
|
|
105
104
|
result.content = await doTest(typeContent, withItems, timeLimit);
|
|
106
105
|
if (result.content.prevented) {
|
|
@@ -118,5 +117,6 @@ exports.reporter = async (page, withItems, withNewContent) => {
|
|
|
118
117
|
console.log('ERROR: Getting ibm test report from URL took too long');
|
|
119
118
|
}
|
|
120
119
|
}
|
|
120
|
+
await close();
|
|
121
121
|
return {result};
|
|
122
122
|
};
|