testaro 2.2.0 → 2.2.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/README.md +4 -0
- package/package.json +1 -1
- package/tests/hover.js +203 -165
package/README.md
CHANGED
|
@@ -377,6 +377,8 @@ The second item in each array, if there are 3 items in the array, is an operator
|
|
|
377
377
|
- `>`: greater than
|
|
378
378
|
- `!`: unequal to
|
|
379
379
|
|
|
380
|
+
A typical use for an `expect` property is checking the correctness of a Testaro test. Thus, the validation scripts in the `validation/tests/scripts` directory all contain `test` commands with `expect` properties. See the “Validation” section below.
|
|
381
|
+
|
|
380
382
|
## Batches
|
|
381
383
|
|
|
382
384
|
There are two ways to use a script to give instructions to Testaro:
|
|
@@ -447,6 +449,8 @@ The executors are:
|
|
|
447
449
|
|
|
448
450
|
To execute any executor `xyz.js`, call it with the statement `node validation/executors/xyz`. The results will appear in the standard output.
|
|
449
451
|
|
|
452
|
+
The `tests.js` executor makes use of the scripts in the `validation/tests/scripts` directory, and they, in turn, run tests on HTML files in the `validation/tests/targets` directory.
|
|
453
|
+
|
|
450
454
|
## Contribution
|
|
451
455
|
|
|
452
456
|
You can define additional Testaro commands and functionality. Contributions are welcome.
|
package/package.json
CHANGED
package/tests/hover.js
CHANGED
|
@@ -8,188 +8,226 @@
|
|
|
8
8
|
examined are the descendants of the grandparent of the element hovered over if that element
|
|
9
9
|
has the tag name 'A' or 'BUTTON' or otherwise the descendants of the element. The only
|
|
10
10
|
elements counted as being made visible by hovering are those with tag names 'A', 'BUTTON',
|
|
11
|
-
'INPUT', and 'SPAN', and those with 'role="menuitem"' attributes.
|
|
11
|
+
'INPUT', and 'SPAN', and those with 'role="menuitem"' attributes. The test waits 700 ms after
|
|
12
|
+
each hover in case of delayed effects. Despite this delay, the test makes the execution time
|
|
13
|
+
practical by randomly sampling targets instead of hovering over all of them. Therefore, the
|
|
14
|
+
results may vary from one execution to another.
|
|
12
15
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
16
|
+
|
|
17
|
+
// CONSTANTS
|
|
18
|
+
|
|
19
|
+
// Selectors of active elements likely to be disclosed by a hover.
|
|
20
|
+
const targetSelectors = ['a', 'button', 'input', '[role=menuitem]', 'span']
|
|
21
|
+
.map(selector => `${selector}:visible`)
|
|
22
|
+
.join(', ');
|
|
23
|
+
// Initialize the result.
|
|
24
|
+
const data = {
|
|
25
|
+
totals: {
|
|
26
|
+
triggers: 0,
|
|
27
|
+
madeVisible: 0,
|
|
28
|
+
opacityChanged: 0,
|
|
29
|
+
opacityAffected: 0,
|
|
30
|
+
unhoverables: 0
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// VARIABLES
|
|
35
|
+
|
|
36
|
+
// Counter.
|
|
37
|
+
let elementsChecked = 0;
|
|
38
|
+
|
|
39
|
+
// FUNCTIONS
|
|
40
|
+
|
|
41
|
+
// Samples a population and returns the sample.
|
|
42
|
+
const getSample = (population, sampleSize) => {
|
|
43
|
+
const popSize = population.length;
|
|
44
|
+
if (sampleSize > 0 && sampleSize < popSize) {
|
|
45
|
+
const sample = new Set();
|
|
46
|
+
while (sample.size < sampleSize) {
|
|
47
|
+
const index = Math.floor(popSize * Math.random());
|
|
48
|
+
sample.add(population[index]);
|
|
36
49
|
}
|
|
37
|
-
|
|
50
|
+
return Array.from(sample);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
// Recursively finds and reports triggers and targets.
|
|
57
|
+
const find = async (withItems, page, triggers) => {
|
|
38
58
|
if (withItems) {
|
|
39
59
|
data.items = {
|
|
40
60
|
triggers: [],
|
|
41
61
|
unhoverables: []
|
|
42
62
|
};
|
|
43
63
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
firstTrigger => {
|
|
64
|
-
const parent = firstTrigger.parentElement;
|
|
65
|
-
if (parent) {
|
|
66
|
-
return parent.parentElement || parent;
|
|
67
|
-
}
|
|
68
|
-
else {
|
|
69
|
-
return firstTrigger;
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
firstTrigger
|
|
73
|
-
);
|
|
74
|
-
root = rootJSHandle.asElement();
|
|
75
|
-
}
|
|
76
|
-
// Identify the visible active descendants of the root before the hover.
|
|
77
|
-
const preVisibles = await root.$$(targetSelectors);
|
|
78
|
-
// Identify all the descendants of the root.
|
|
79
|
-
const descendants = await root.$$('*');
|
|
80
|
-
// Identify their opacities before the hover.
|
|
81
|
-
const preOpacities = await page.evaluate(
|
|
82
|
-
elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
|
|
83
|
-
);
|
|
84
|
-
try {
|
|
85
|
-
// Hover over the potential trigger.
|
|
86
|
-
await firstTrigger.hover({timeout: 700});
|
|
87
|
-
// Identify whether it controls other elements.
|
|
88
|
-
const isController = await page.evaluate(
|
|
89
|
-
element => element.ariaHasPopup || element.hasAttribute('aria-controls'), firstTrigger
|
|
90
|
-
);
|
|
91
|
-
// Wait for any delayed and/or slowed hover reaction if likely.
|
|
92
|
-
await page.waitForTimeout(
|
|
93
|
-
elementsChecked++ < 10 || tagName !== triggerTag || isController ? 1200 : 200
|
|
94
|
-
);
|
|
95
|
-
await root.waitForElementState('stable');
|
|
96
|
-
// Identify the visible active descendants of the root during the hover.
|
|
97
|
-
const postVisibles = await root.$$(targetSelectors);
|
|
98
|
-
// Identify the opacities of the descendants of the root during the hover.
|
|
99
|
-
const postOpacities = await page.evaluate(
|
|
100
|
-
elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
|
|
101
|
-
);
|
|
102
|
-
// Identify the elements with opacity changes.
|
|
103
|
-
const opacityTargets = descendants
|
|
104
|
-
.filter((descendant, index) => postOpacities[index] !== preOpacities[index]);
|
|
105
|
-
// Count them and their descendants.
|
|
106
|
-
const opacityAffected = opacityTargets.length
|
|
107
|
-
? await page.evaluate(elements => elements.reduce(
|
|
108
|
-
(total, current) => total + 1 + current.querySelectorAll('*').length, 0
|
|
109
|
-
), opacityTargets)
|
|
110
|
-
: 0;
|
|
111
|
-
// If hovering disclosed any element or changed any opacity:
|
|
112
|
-
if (postVisibles.length > preVisibles.length || opacityAffected) {
|
|
113
|
-
// Preserve the lengthened reaction wait, if any, for the next 5 tries.
|
|
114
|
-
if (elementsChecked < 11) {
|
|
115
|
-
elementsChecked = 5;
|
|
64
|
+
// If any potential disclosure triggers remain:
|
|
65
|
+
if (triggers.length) {
|
|
66
|
+
// Identify the first of them.
|
|
67
|
+
const firstTrigger = triggers[0];
|
|
68
|
+
const tagNameJSHandle = await firstTrigger.getProperty('tagName')
|
|
69
|
+
.catch(error => {
|
|
70
|
+
console.log(`ERROR getting trigger tag name (${error.message})`);
|
|
71
|
+
return '';
|
|
72
|
+
});
|
|
73
|
+
if (tagNameJSHandle) {
|
|
74
|
+
const tagName = await tagNameJSHandle.jsonValue();
|
|
75
|
+
// Identify the root of a subtree likely to contain disclosed elements.
|
|
76
|
+
let root = firstTrigger;
|
|
77
|
+
if (['A', 'BUTTON'].includes(tagName)) {
|
|
78
|
+
const rootJSHandle = await page.evaluateHandle(
|
|
79
|
+
firstTrigger => {
|
|
80
|
+
const parent = firstTrigger.parentElement;
|
|
81
|
+
if (parent) {
|
|
82
|
+
return parent.parentElement || parent;
|
|
116
83
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
position: {
|
|
120
|
-
x: 0,
|
|
121
|
-
y: 0
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
// Wait for any delayed and/or slowed hover reaction.
|
|
125
|
-
await page.waitForTimeout(200);
|
|
126
|
-
await root.waitForElementState('stable');
|
|
127
|
-
// Increment the counts of triggers and targets.
|
|
128
|
-
data.totals.triggers++;
|
|
129
|
-
const madeVisible = Math.max(0, postVisibles.length - preVisibles.length);
|
|
130
|
-
data.totals.madeVisible += madeVisible;
|
|
131
|
-
data.totals.opacityChanged += opacityTargets.length;
|
|
132
|
-
data.totals.opacityAffected += opacityAffected;
|
|
133
|
-
// If details are to be reported:
|
|
134
|
-
if (withItems) {
|
|
135
|
-
// Report them.
|
|
136
|
-
const triggerDataJSHandle = await page.evaluateHandle(args => {
|
|
137
|
-
// Returns the text of an element.
|
|
138
|
-
const textOf = (element, limit) => {
|
|
139
|
-
const text = element.textContent.trim() || element.outerHTML.trim();
|
|
140
|
-
return text.replace(/\s{2,}/sg, ' ').slice(0, limit);
|
|
141
|
-
};
|
|
142
|
-
const trigger = args[0];
|
|
143
|
-
const preVisibles = args[1];
|
|
144
|
-
const postVisibles = args[2];
|
|
145
|
-
const madeVisible = postVisibles
|
|
146
|
-
.filter(el => ! preVisibles.includes(el))
|
|
147
|
-
.map(el => ({
|
|
148
|
-
tagName: el.tagName,
|
|
149
|
-
text: textOf(el, 50)
|
|
150
|
-
}));
|
|
151
|
-
const opacityChanged = args[3].map(el => ({
|
|
152
|
-
tagName: el.tagName,
|
|
153
|
-
text: textOf(el, 50)
|
|
154
|
-
}));
|
|
155
|
-
return {
|
|
156
|
-
tagName: trigger.tagName,
|
|
157
|
-
id: trigger.id || '',
|
|
158
|
-
text: textOf(trigger, 50),
|
|
159
|
-
madeVisible,
|
|
160
|
-
opacityChanged
|
|
161
|
-
};
|
|
162
|
-
}, [firstTrigger, preVisibles, postVisibles, opacityTargets]);
|
|
163
|
-
const triggerData = await triggerDataJSHandle.jsonValue();
|
|
164
|
-
data.items.triggers.push(triggerData);
|
|
84
|
+
else {
|
|
85
|
+
return firstTrigger;
|
|
165
86
|
}
|
|
87
|
+
},
|
|
88
|
+
firstTrigger
|
|
89
|
+
);
|
|
90
|
+
root = rootJSHandle.asElement();
|
|
91
|
+
}
|
|
92
|
+
// Identify the visible active descendants of the root before the hover.
|
|
93
|
+
const preVisibles = await root.$$(targetSelectors);
|
|
94
|
+
// Identify all the descendants of the root.
|
|
95
|
+
const descendants = await root.$$('*');
|
|
96
|
+
// Identify their opacities before the hover.
|
|
97
|
+
const preOpacities = await page.evaluate(
|
|
98
|
+
elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
|
|
99
|
+
);
|
|
100
|
+
try {
|
|
101
|
+
// Hover over the potential trigger.
|
|
102
|
+
await firstTrigger.hover({timeout: 700});
|
|
103
|
+
// Identify whether it is coded as controlling other elements.
|
|
104
|
+
const isController = await page.evaluate(
|
|
105
|
+
element => element.ariaHasPopup || element.hasAttribute('aria-controls'), firstTrigger
|
|
106
|
+
);
|
|
107
|
+
// Wait for any delayed and/or slowed hover reaction, longer if coded as a controller.
|
|
108
|
+
await page.waitForTimeout(isController ? 1200 : 600);
|
|
109
|
+
await root.waitForElementState('stable');
|
|
110
|
+
// Identify the visible active descendants of the root during the hover.
|
|
111
|
+
const postVisibles = await root.$$(targetSelectors);
|
|
112
|
+
// Identify the opacities of the descendants of the root during the hover.
|
|
113
|
+
const postOpacities = await page.evaluate(
|
|
114
|
+
elements => elements.map(el => window.getComputedStyle(el).opacity), descendants
|
|
115
|
+
);
|
|
116
|
+
// Identify the elements with opacity changes.
|
|
117
|
+
const opacityTargets = descendants
|
|
118
|
+
.filter((descendant, index) => postOpacities[index] !== preOpacities[index]);
|
|
119
|
+
// Count them and their descendants.
|
|
120
|
+
const opacityAffected = opacityTargets.length
|
|
121
|
+
? await page.evaluate(elements => elements.reduce(
|
|
122
|
+
(total, current) => total + 1 + current.querySelectorAll('*').length, 0
|
|
123
|
+
), opacityTargets)
|
|
124
|
+
: 0;
|
|
125
|
+
// If hovering disclosed any element or changed any opacity:
|
|
126
|
+
if (postVisibles.length > preVisibles.length || opacityAffected) {
|
|
127
|
+
// Preserve the lengthened reaction wait, if any, for the next 5 tries.
|
|
128
|
+
if (elementsChecked < 11) {
|
|
129
|
+
elementsChecked = 5;
|
|
166
130
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
131
|
+
// Hover over the upper-left corner of the page, to undo any hover reactions.
|
|
132
|
+
await page.hover('body', {
|
|
133
|
+
position: {
|
|
134
|
+
x: 0,
|
|
135
|
+
y: 0
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
// Wait for any delayed and/or slowed hover reaction.
|
|
139
|
+
await page.waitForTimeout(200);
|
|
140
|
+
await root.waitForElementState('stable');
|
|
141
|
+
// Increment the counts of triggers and targets.
|
|
142
|
+
data.totals.triggers++;
|
|
143
|
+
const madeVisible = Math.max(0, postVisibles.length - preVisibles.length);
|
|
144
|
+
data.totals.madeVisible += madeVisible;
|
|
145
|
+
data.totals.opacityChanged += opacityTargets.length;
|
|
146
|
+
data.totals.opacityAffected += opacityAffected;
|
|
147
|
+
// If details are to be reported:
|
|
177
148
|
if (withItems) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
149
|
+
// Report them.
|
|
150
|
+
const triggerDataJSHandle = await page.evaluateHandle(args => {
|
|
151
|
+
// Returns the text of an element.
|
|
152
|
+
const textOf = (element, limit) => {
|
|
153
|
+
const text = element.textContent.trim() || element.outerHTML.trim();
|
|
154
|
+
return text.replace(/\s{2,}/sg, ' ').slice(0, limit);
|
|
155
|
+
};
|
|
156
|
+
const trigger = args[0];
|
|
157
|
+
const preVisibles = args[1];
|
|
158
|
+
const postVisibles = args[2];
|
|
159
|
+
const madeVisible = postVisibles
|
|
160
|
+
.filter(el => ! preVisibles.includes(el))
|
|
161
|
+
.map(el => ({
|
|
162
|
+
tagName: el.tagName,
|
|
163
|
+
text: textOf(el, 50)
|
|
164
|
+
}));
|
|
165
|
+
const opacityChanged = args[3].map(el => ({
|
|
166
|
+
tagName: el.tagName,
|
|
167
|
+
text: textOf(el, 50)
|
|
168
|
+
}));
|
|
169
|
+
return {
|
|
170
|
+
tagName: trigger.tagName,
|
|
171
|
+
id: trigger.id || '',
|
|
172
|
+
text: textOf(trigger, 50),
|
|
173
|
+
madeVisible,
|
|
174
|
+
opacityChanged
|
|
175
|
+
};
|
|
176
|
+
}, [firstTrigger, preVisibles, postVisibles, opacityTargets]);
|
|
177
|
+
const triggerData = await triggerDataJSHandle.jsonValue();
|
|
178
|
+
data.items.triggers.push(triggerData);
|
|
183
179
|
}
|
|
184
180
|
}
|
|
185
|
-
triggerTag = tagName;
|
|
186
181
|
}
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
catch (error) {
|
|
183
|
+
console.log('ERROR hovering');
|
|
184
|
+
// Returns the text of an element.
|
|
185
|
+
const textOf = async (element, limit) => {
|
|
186
|
+
let text = await element.textContent();
|
|
187
|
+
text = text.trim() || await element.innerHTML();
|
|
188
|
+
return text.trim().replace(/\s*/sg, '').slice(0, limit);
|
|
189
|
+
};
|
|
190
|
+
data.totals.unhoverables++;
|
|
191
|
+
if (withItems) {
|
|
192
|
+
data.items.unhoverables.push({
|
|
193
|
+
tagName: tagName,
|
|
194
|
+
id: firstTrigger.id || '',
|
|
195
|
+
text: await textOf(firstTrigger, 50)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
189
199
|
}
|
|
190
|
-
|
|
200
|
+
// Process the remaining potential triggers.
|
|
201
|
+
await find(withItems, page, triggers.slice(1));
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
exports.reporter = async (page, withItems) => {
|
|
205
|
+
// Identify the triggers.
|
|
206
|
+
const selectors = [
|
|
207
|
+
'body a:visible',
|
|
208
|
+
'body button:visible',
|
|
209
|
+
'body li:visible, body [onmouseenter]:visible',
|
|
210
|
+
'body [onmouseover]:visible'
|
|
211
|
+
];
|
|
212
|
+
const triggers = await page.$$(selectors.join(', '))
|
|
213
|
+
.catch(error => {
|
|
214
|
+
console.log(`ERROR getting hover triggers (${error.message})`);
|
|
215
|
+
return [];
|
|
216
|
+
});
|
|
217
|
+
// If they number more than the sample size limit, sample them.
|
|
218
|
+
const triggerCount = triggers.length;
|
|
219
|
+
const sampleSize = 15;
|
|
220
|
+
const triggerSample = triggerCount > sampleSize ? getSample(triggers, 15) : triggers;
|
|
191
221
|
// Find and document the hover-triggered disclosures.
|
|
192
|
-
await find(
|
|
222
|
+
await find(withItems, page, triggerSample);
|
|
223
|
+
// If the triggers were sampled:
|
|
224
|
+
if (triggerCount > sampleSize) {
|
|
225
|
+
// Change the totals to population estimates.
|
|
226
|
+
const multiplier = triggerCount / sampleSize;
|
|
227
|
+
Object.keys(data.totals).forEach(key => {
|
|
228
|
+
data.totals[key] = Math.round(multiplier * data.totals[key]);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
193
231
|
// Return the result.
|
|
194
232
|
return {result: data};
|
|
195
233
|
};
|