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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "16.1.0",
3
+ "version": "16.3.0",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
+ };
@@ -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: false,
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
- // Make a screenshot.
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 = Math.round(100 * pixelChanges / (width * height));
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
+ };