testaro 5.4.0 → 5.5.2

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/create.js CHANGED
@@ -89,7 +89,8 @@ exports.runJob = async (scriptID, batchID) => {
89
89
  // If there is no need to keep checking:
90
90
  const reportNames = await fs.readdir(reportDir);
91
91
  const timedOut = Date.now() - startTime > 1000 * timeLimit;
92
- if (timedOut || reportNames.includes(`${id}.json`) || ! childAlive) {
92
+ const reportWritten = reportNames.includes(`${id}.json`);
93
+ if (timedOut || reportWritten || ! childAlive) {
93
94
  // Stop checking.
94
95
  clearInterval(reCheck);
95
96
  // If the cause is a timeout:
@@ -98,7 +99,7 @@ exports.runJob = async (scriptID, batchID) => {
98
99
  timeoutHosts.push(id);
99
100
  }
100
101
  // Otherwise, if the cause is a child crash:
101
- else if (! childAlive) {
102
+ else if (! (childAlive || reportWritten)) {
102
103
  // Add the host to the array of crashed hosts.
103
104
  crashHosts.push(id);
104
105
  }
@@ -121,7 +122,9 @@ exports.runJob = async (scriptID, batchID) => {
121
122
  console.log(`Reports not created:\n${JSON.stringify(timeoutHosts), null, 2}`);
122
123
  }
123
124
  if (crashHosts.length) {
124
- console.log(`Hosts crashed:\n${JSON.stringify(crashHosts), null, 2}`);
125
+ console.log(
126
+ `Hosts crashed with or without report:\n${JSON.stringify(crashHosts, null, 2)}`
127
+ );
125
128
  }
126
129
  }
127
130
  };
package/data/roles.txt ADDED
@@ -0,0 +1,62 @@
1
+ input type=checkbox role=checkbox
2
+ td role=cell if the ancestor table element is exposed as a role=table
3
+ td role=gridcell if the ancestor table element is exposed as a role=grid or treegrid
4
+ th role=columnheader, rowheader or cell if the ancestor table element is exposed as a role=table
5
+ td role=columnheader, rowheader or gridcell if the ancestor table element is exposed as a role=grid or treegrid
6
+ dd role=definition
7
+ dt role=term
8
+ hr role=separator
9
+ li role=listitem
10
+ ol role=list
11
+ tr role=row
12
+ ul role=list
13
+ dfn role=term
14
+ nav role=navigation
15
+ SVG role=graphics-document
16
+ html role=document
17
+ main role=main
18
+ math role=math
19
+ menu role=list
20
+ aside role=complementary
21
+ table role=table
22
+ tbody role=rowgroup
23
+ tfoot role=rowgroup
24
+ thead role=rowgroup
25
+ button role=button
26
+ dialog role=dialog
27
+ figure role=figure
28
+ output role=status
29
+ article role=article
30
+ details role=group
31
+ section role=region if the section element has an accessible name
32
+ summary role=button
33
+ datalist role=listbox
34
+ fieldset role=group
35
+ h1 to h6 role=heading, aria-level = the number in the element's tag name
36
+ \
37
+ progress role=progressbar
38
+ textarea role=textbox
39
+ a with href role=link
40
+ area with href role=link
41
+ img with alt="" role=presentation
42
+ input type=image role=button
43
+ input type=radio role=radio
44
+ input type=range role=slider
45
+ input type=reset role=button
46
+ input type=button role=button
47
+ input type=number role=spinbutton
48
+ input type=submit role=button
49
+ img with alt="some text" role=img
50
+ img without an alt attribute role=img
51
+ input type=url with no list attribute role=textbox
52
+ input type=tel, with no list attribute role=textbox
53
+ input type=email with no list attribute role=textbox
54
+ input type=search, with no list attribute role=searchbox
55
+ form If the form element has an accessible name: role=form. Otherwise, no corresponding role.
56
+ input type=text or with a missing or invalid type, with no list attribute role=textbox
57
+ select (with a multiple attribute or a size attribute having value greater than 1) role=listbox
58
+ select (with NO multiple attribute and NO size attribute having value greater than 1) role=combobox
59
+ option element that is in a list of options or that represents a suggestion in a datalist role=option
60
+ footer If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=contentinfo. Otherwise no corresponding role.
61
+ header If not a descendant of an article, aside, main, nav or section element, or an element with role=article, complementary, main, navigation or region then role=banner. Otherwise no corresponding role
62
+ input type=text, search, tel, url, email, or with a missing or invalid type, with a list attribute role=combobox
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testaro",
3
- "version": "5.4.0",
3
+ "version": "5.5.2",
4
4
  "description": "Automation of accessibility testing",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/tests/focInd.js CHANGED
@@ -138,7 +138,7 @@ exports.reporter = async (page, revealAll, allowedDelay, withItems) => {
138
138
  const hasIndicator
139
139
  = hasDiffOutline
140
140
  || hasDiffBorder
141
- || diff('box-shadow')
141
+ || diff('boxShadow')
142
142
  || diff('fontSize')
143
143
  || diff('fontStyle')
144
144
  || diff('textDecorationLine')
package/tests/hover.js CHANGED
@@ -28,10 +28,6 @@
28
28
  value in the same location being the target.
29
29
  */
30
30
 
31
- // CONSTANTS
32
-
33
- const data = {};
34
-
35
31
  // FUNCTIONS
36
32
 
37
33
  // Samples a population and returns the sample.
@@ -62,14 +58,13 @@ const textOf = async (element, limit) => {
62
58
  return text.trim().replace(/\s*/sg, '').slice(0, limit);
63
59
  };
64
60
  // Recursively reports impacts of hovering over triggers.
65
- const find = async (withItems, page, region, sample, popRatio) => {
61
+ const find = async (data, withItems, page, region, sample, popRatio) => {
66
62
  // If any potential triggers remain:
67
63
  if (sample.length) {
68
64
  // Identify the first of them.
69
65
  const firstTrigger = sample[0];
70
66
  const tagNameJSHandle = await firstTrigger.getProperty('tagName')
71
67
  .catch(error => {
72
- console.log(`ERROR getting trigger tag name (${error.message})`);
73
68
  return '';
74
69
  });
75
70
  if (tagNameJSHandle) {
@@ -89,7 +84,7 @@ const find = async (withItems, page, region, sample, popRatio) => {
89
84
  root = rootJSHandle.asElement();
90
85
  }
91
86
  // Identify all the descendants of the root.
92
- const preDescendants = await root.$$('*');
87
+ const preDescendants = await root.$$(':visible');
93
88
  // Identify their opacities.
94
89
  const preOpacities = await page.evaluate(elements => elements.map(
95
90
  element => window.getComputedStyle(element).opacity
@@ -102,8 +97,11 @@ const find = async (withItems, page, region, sample, popRatio) => {
102
97
  });
103
98
  // Repeatedly seeks impacts.
104
99
  const getImpacts = async (interval, triesLeft) => {
100
+ // If the allowed trial count has not yet been exhausted:
105
101
  if (triesLeft--) {
106
- const postDescendants = await root.$$('*');
102
+ // Get the collection of descendants of the root.
103
+ const postDescendants = await root.$$(':visible');
104
+ // Identify the prior descandants of the root still in existence.
107
105
  const remainerIndexes = await page.evaluate(args => {
108
106
  const preDescendants = args[0];
109
107
  const postDescendants = args[1];
@@ -112,6 +110,7 @@ const find = async (withItems, page, region, sample, popRatio) => {
112
110
  .filter(index => index > -1);
113
111
  return remainerIndexes;
114
112
  }, [preDescendants, postDescendants]);
113
+ // Get the count of elements added by the hover event.
115
114
  const additionCount = postDescendants.length - remainerIndexes.length;
116
115
  const removalCount = preDescendants.length - remainerIndexes.length;
117
116
  const remainers = [];
@@ -191,17 +190,22 @@ const find = async (withItems, page, region, sample, popRatio) => {
191
190
  console.log(`ERROR hovering (${error.message.replace(/\n.+/s, '')})`);
192
191
  data.totals.unhoverables++;
193
192
  if (withItems) {
194
- const id = await firstTrigger.getAttribute('id');
195
- data.items[region].unhoverables.push({
196
- tagName,
197
- id: id || '',
198
- text: await textOf(firstTrigger, 50)
199
- });
193
+ try {
194
+ const id = await firstTrigger.getAttribute('id');
195
+ data.items[region].unhoverables.push({
196
+ tagName,
197
+ id: id || '',
198
+ text: await textOf(firstTrigger, 50)
199
+ });
200
+ }
201
+ catch(error) {
202
+ console.log('ERROR itemizing unhoverable element');
203
+ }
200
204
  }
201
205
  }
202
206
  }
203
207
  // Process the remaining potential triggers.
204
- await find(withItems, page, region, sample.slice(1), popRatio);
208
+ await find(data, withItems, page, region, sample.slice(1), popRatio);
205
209
  }
206
210
  };
207
211
  // Performs the hover test and reports results.
@@ -209,16 +213,18 @@ exports.reporter = async (
209
213
  page, headSize = 0, headSampleSize = -1, tailSampleSize = -1, withItems
210
214
  ) => {
211
215
  // Initialize the result.
212
- data.totals = {
213
- triggers: 0,
214
- headTriggers: 0,
215
- tailTriggers: 0,
216
- impactTriggers: 0,
217
- additions: 0,
218
- removals: 0,
219
- opacityChanges: 0,
220
- opacityImpact: 0,
221
- unhoverables: 0
216
+ const data = {
217
+ totals: {
218
+ triggers: 0,
219
+ headTriggers: 0,
220
+ tailTriggers: 0,
221
+ impactTriggers: 0,
222
+ additions: 0,
223
+ removals: 0,
224
+ opacityChanges: 0,
225
+ opacityImpact: 0,
226
+ unhoverables: 0
227
+ }
222
228
  };
223
229
  // If details are to be reported:
224
230
  if (withItems) {
@@ -255,10 +261,10 @@ exports.reporter = async (
255
261
  const tailSample = tailSampleSize === -1 ? tailTriggers : getSample(tailTriggers, tailSampleSize);
256
262
  // Find and document the impacts.
257
263
  if (headSample.length) {
258
- await find(withItems, page, 'head', headSample, headTriggerCount / headSample.length);
264
+ await find(data, withItems, page, 'head', headSample, headTriggerCount / headSample.length);
259
265
  }
260
266
  if (tailSample.length) {
261
- await find(withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
267
+ await find(data, withItems, page, 'tail', tailSample, tailTriggerCount / tailSample.length);
262
268
  }
263
269
  // Round the reported totals.
264
270
  Object.keys(data.totals).forEach(key => {
package/tests/role.js CHANGED
@@ -133,12 +133,6 @@ exports.reporter = async page => await page.$eval('body', body => {
133
133
  dt: 'term',
134
134
  fieldset: 'group',
135
135
  figure: 'figure',
136
- h1: 'heading',
137
- h2: 'heading',
138
- h3: 'heading',
139
- h4: 'heading',
140
- h5: 'heading',
141
- h6: 'heading',
142
136
  hr: 'separator',
143
137
  html: 'document',
144
138
  li: 'listitem',
@@ -159,6 +153,207 @@ exports.reporter = async page => await page.$eval('body', body => {
159
153
  tr: 'row',
160
154
  ul: 'list'
161
155
  };
156
+ const implicitAttributes = {
157
+ a: [
158
+ {
159
+ role: 'link',
160
+ attributes: {
161
+ href: /./
162
+ }
163
+ }
164
+ ],
165
+ area: [
166
+ {
167
+ role: 'link',
168
+ attributes: {
169
+ href: /./
170
+ }
171
+ }
172
+ ],
173
+ h1: [
174
+ {
175
+ role: 'heading',
176
+ attributes: {
177
+ 'aria-level': /^1$/
178
+ }
179
+ }
180
+ ],
181
+ h2: [
182
+ {
183
+ role: 'heading',
184
+ attributes: {
185
+ 'aria-level': /^2$/
186
+ }
187
+ },
188
+ {
189
+ role: 'heading',
190
+ attributes: {
191
+ 'aria-level': false
192
+ }
193
+ }
194
+ ],
195
+ h3: [
196
+ {
197
+ role: 'heading',
198
+ attributes: {
199
+ 'aria-level': /^3$/
200
+ }
201
+ }
202
+ ],
203
+ h4: [
204
+ {
205
+ role: 'heading',
206
+ attributes: {
207
+ 'aria-level': /^4$/
208
+ }
209
+ }
210
+ ],
211
+ h5: [
212
+ {
213
+ role: 'heading',
214
+ attributes: {
215
+ 'aria-level': /^5$/
216
+ }
217
+ }
218
+ ],
219
+ h6: [
220
+ {
221
+ role: 'heading',
222
+ attributes: {
223
+ 'aria-level': /^6$/
224
+ }
225
+ }
226
+ ],
227
+ input: [
228
+ {
229
+ role: 'checkbox',
230
+ attributes: {
231
+ type: /^checkbox$/
232
+ }
233
+ },
234
+ {
235
+ role: 'button',
236
+ attributes: {
237
+ type: /^(?:button|image|reset|submit)$/
238
+ }
239
+ },
240
+ {
241
+ role: 'combobox',
242
+ attributes: {
243
+ type: /^(?:email|search|tel|text|url)$/,
244
+ list: true
245
+ }
246
+ },
247
+ {
248
+ role: 'combobox',
249
+ attributes: {
250
+ type: false,
251
+ list: true
252
+ }
253
+ },
254
+ {
255
+ role: 'radio',
256
+ attributes: {
257
+ type: /^radio$/
258
+ }
259
+ },
260
+ {
261
+ role: 'searchbox',
262
+ attributes: {
263
+ type: /^search$/,
264
+ list: false
265
+ }
266
+ },
267
+ {
268
+ role: 'slider',
269
+ attributes: {
270
+ type: /^range$/
271
+ }
272
+ },
273
+ {
274
+ role: 'spinbutton',
275
+ attributes: {
276
+ type: /^number$/
277
+ }
278
+ },
279
+ {
280
+ role: 'textbox',
281
+ attributes: {
282
+ type: /^(?:email|tel|text|url)$/,
283
+ list: false
284
+ }
285
+ },
286
+ {
287
+ role: 'textbox',
288
+ attributes: {
289
+ type: false,
290
+ list: false
291
+ }
292
+ },
293
+ {
294
+ role: 'checkbox',
295
+ attributes: {
296
+ type: /^checkbox$/
297
+ }
298
+ },
299
+ {
300
+ role: 'checkbox',
301
+ attributes: {
302
+ type: /^checkbox$/
303
+ }
304
+ },
305
+ ],
306
+ img: [
307
+ {
308
+ role: 'presentation',
309
+ attributes: {
310
+ alt: /^$/
311
+ }
312
+ },
313
+ {
314
+ role: 'img',
315
+ attributes: {
316
+ alt: /./
317
+ }
318
+ },
319
+ {
320
+ role: 'img',
321
+ attributes: {
322
+ alt: false
323
+ }
324
+ }
325
+ ],
326
+ select: [
327
+ {
328
+ role: 'listbox',
329
+ attributes: {
330
+ multiple: true
331
+ }
332
+ },
333
+ {
334
+ role: 'listbox',
335
+ attributes: {
336
+ size: /^(?:[2-9]|[1-9]\d+)$/
337
+ }
338
+ },
339
+ {
340
+ role: 'combobox',
341
+ attributes: {
342
+ multiple: false,
343
+ size: false
344
+ }
345
+ },
346
+ {
347
+ role: 'combobox',
348
+ attributes: {
349
+ multiple: false,
350
+ size: /^1$/
351
+ }
352
+ }
353
+ ]
354
+ };
355
+ // Array of th and td elements with redundant roles.
356
+ const redundantCells = [];
162
357
  // FUNCTIONS
163
358
  const dataInit = (data, tagName, role) => {
164
359
  if (! data.tagNames[tagName]) {
@@ -171,6 +366,17 @@ exports.reporter = async page => await page.$eval('body', body => {
171
366
  };
172
367
  }
173
368
  };
369
+ const tallyTableRedundancy = (elements, okRoles, tagName) => {
370
+ elements.forEach(element => {
371
+ const role = element.getAttribute('role');
372
+ if (okRoles.includes(role)) {
373
+ dataInit(data, tagName, role);
374
+ data.redundantRoleElements++;
375
+ data.tagNames[tagName][role].redundant++;
376
+ redundantCells.push(element);
377
+ }
378
+ });
379
+ };
174
380
  // OPERATION
175
381
  // Remove the deprecated roles from the non-abstract roles.
176
382
  goodRoles.forEach(role => {
@@ -187,8 +393,25 @@ exports.reporter = async page => await page.$eval('body', body => {
187
393
  redundantRoleElements: 0,
188
394
  tagNames: {}
189
395
  };
190
- // Identify the elements with redundant roles and bad roles.
191
- roleElements.forEach(element => {
396
+ // Identify the th and td elements with redundant roles.
397
+ const gridHeaders = Array.from(
398
+ document.body.querySelectorAll('table[role=grid] th, table[role=treegrid] th')
399
+ );
400
+ const gridCells = Array.from(
401
+ document.body.querySelectorAll('table[role=grid] td, table[role=treegrid] td')
402
+ );
403
+ const tableHeaders = Array.from(
404
+ document.body.querySelectorAll('table[role=table] th, table:not([role]) th')
405
+ );
406
+ const tableCells = Array.from(
407
+ document.body.querySelectorAll('table[role=table] td, table:not([role]) td')
408
+ );
409
+ tallyTableRedundancy(gridHeaders, ['columnheader', 'rowheader', 'gridcell'], 'TH');
410
+ tallyTableRedundancy(gridCells, ['gridcell'], 'TD');
411
+ tallyTableRedundancy(tableHeaders, ['columnheader', 'rowheader', 'cell'], 'TH');
412
+ tallyTableRedundancy(tableCells, ['cell'], 'TD');
413
+ // Identify the additional elements with redundant roles and bad roles.
414
+ roleElements.filter(element => ! redundantCells.includes(element)).forEach(element => {
192
415
  const role = element.getAttribute('role');
193
416
  const tagName = element.tagName;
194
417
  // If the role is not absolutely valid:
@@ -196,11 +419,37 @@ exports.reporter = async page => await page.$eval('body', body => {
196
419
  // If it is bad or redundant:
197
420
  if (badRoles.has(role)) {
198
421
  dataInit(data, tagName, role);
199
- // Add the facts to the result.
200
- if (role === implicitRoles[tagName.toLowerCase()]) {
422
+ const lcTagName = tagName.toLowerCase();
423
+ // If it is simply redundant:
424
+ if (role === implicitRoles[lcTagName]) {
425
+ data.redundantRoleElements++;
426
+ data.tagNames[tagName][role].redundant++;
427
+ }
428
+ // Otherwise, if it is attributionally redundant:
429
+ else if (
430
+ implicitAttributes[lcTagName] && implicitAttributes[lcTagName].some(
431
+ criterion => role === criterion.role && Object.keys(criterion.attributes).every(
432
+ attributeName => {
433
+ const rule = criterion.attributes[attributeName];
434
+ const exists = element.hasAttribute(attributeName);
435
+ const value = exists ? element.getAttribute(attributeName) : null;
436
+ if (rule === true) {
437
+ return exists;
438
+ }
439
+ else if (rule === false) {
440
+ return ! exists;
441
+ }
442
+ else {
443
+ return rule.test(value);
444
+ }
445
+ }
446
+ )
447
+ )
448
+ ) {
201
449
  data.redundantRoleElements++;
202
450
  data.tagNames[tagName][role].redundant++;
203
451
  }
452
+ // Otherwise, i.e. if it is absolutely invalid:
204
453
  else {
205
454
  data.badRoleElements++;
206
455
  data.tagNames[tagName][role].bad++;
@@ -0,0 +1,62 @@
1
+ // app.js
2
+ // Validator for Testaro tests.
3
+
4
+ const fs = require('fs').promises;
5
+ const {handleRequest} = require(`${__dirname}/../../run`);
6
+ const validateTests = async () => {
7
+ const totals = {
8
+ attempts: 0,
9
+ successes: 0
10
+ };
11
+ const scriptFileNames = await fs.readdir(`${__dirname}/../tests/scripts`);
12
+ for (const scriptFileName of ['hover.json']) {
13
+ const rawScriptJSON = await fs
14
+ .readFile(`${__dirname}/../tests/scripts/${scriptFileName}`, 'utf8');
15
+ const scriptJSON = rawScriptJSON
16
+ .replace(/__targets__/g, `file://${__dirname}/../tests/targets`);
17
+ const script = JSON.parse(scriptJSON);
18
+ const report = {script};
19
+ report.log = [];
20
+ report.acts = [];
21
+ await handleRequest(report);
22
+ const {log, acts} = report;
23
+ if (log.length === 2 && log[1].event === 'endTime' && /^\d{4}-.+$/.test(log[1].value)) {
24
+ console.log('Success: Log has been correctly populated');
25
+ }
26
+ else {
27
+ console.log('Failure: Log empty or invalid');
28
+ console.log(JSON.stringify(log, null, 2));
29
+ }
30
+ if (
31
+ acts.length === script.commands.length
32
+ && acts.every(
33
+ act => act.type && act.type === 'test'
34
+ ? act.result && act.result.failureCount !== undefined
35
+ : true
36
+ )
37
+ ) {
38
+ totals.attempts++;
39
+ totals.successes++;
40
+ console.log('Success: Reports have been correctly populated');
41
+ if (acts.every(
42
+ act => act.type === 'test' ? act.result.failureCount === 0 : true
43
+ )) {
44
+ totals.attempts++;
45
+ totals.successes++;
46
+ console.log('Success: No failures');
47
+ }
48
+ else {
49
+ totals.attempts++;
50
+ console.log('Failure: At least one test has at least one failure');
51
+ console.log(JSON.stringify(acts, null, 2));
52
+ }
53
+ }
54
+ else {
55
+ totals.attempts++;
56
+ console.log('Failure: Reports empty or invalid');
57
+ console.log(JSON.stringify(acts, null, 2));
58
+ }
59
+ }
60
+ console.log(`Grand totals: attempts ${totals.attempts}, successes ${totals.successes}`);
61
+ };
62
+ validateTests();
@@ -16,12 +16,19 @@
16
16
  "type": "test",
17
17
  "which": "hover",
18
18
  "what": "hover",
19
- "withItems": false,
19
+ "headSize": 3,
20
+ "headSampleSize": 3,
21
+ "tailSampleSize": 30,
22
+ "withItems": true,
20
23
  "expect": [
21
- ["totals.triggers", "=", 0],
22
- ["totals.madeVisible", "=", 0],
23
- ["totals.opacityChanged", "=", 0],
24
- ["totals.opacityAffected", "=", 0],
24
+ ["totals.triggers", "=", 2],
25
+ ["totals.headTriggers", "=", 2],
26
+ ["totals.tailTriggers", "=", 0],
27
+ ["totals.impactTriggers", "=", 0],
28
+ ["totals.additions", "=", 0],
29
+ ["totals.removals", "=", 0],
30
+ ["totals.opacityChanges", "=", 0],
31
+ ["totals.opacityImpact", "=", 0],
25
32
  ["totals.unhoverables", "=", 0]
26
33
  ]
27
34
  },
@@ -34,12 +41,19 @@
34
41
  "type": "test",
35
42
  "which": "hover",
36
43
  "what": "hover",
37
- "withItems": false,
44
+ "headSize": 3,
45
+ "headSampleSize": 3,
46
+ "tailSampleSize": 30,
47
+ "withItems": true,
38
48
  "expect": [
39
- ["totals.triggers", "=", 2],
40
- ["totals.madeVisible", "=", 2],
41
- ["totals.opacityChanged", "=", 1],
42
- ["totals.opacityAffected", "=", 2],
49
+ ["totals.triggers", "=", 4],
50
+ ["totals.headTriggers", "=", 3],
51
+ ["totals.tailTriggers", "=", 1],
52
+ ["totals.impactTriggers", "=", 2],
53
+ ["totals.additions", "=", 3],
54
+ ["totals.removals", "=", 0],
55
+ ["totals.opacityChanges", "=", 1],
56
+ ["totals.opacityImpact", "=", 1],
43
57
  ["totals.unhoverables", "=", 1]
44
58
  ]
45
59
  }
@@ -18,7 +18,8 @@
18
18
  "what": "role",
19
19
  "expect": [
20
20
  ["roleElements", "=", 1],
21
- ["badRoleElements", "=", 0]
21
+ ["badRoleElements", "=", 0],
22
+ ["redundantRoleElements", "=", 0]
22
23
  ]
23
24
  },
24
25
  {
@@ -31,11 +32,19 @@
31
32
  "which": "role",
32
33
  "what": "role",
33
34
  "expect": [
34
- ["roleElements", "=", 3],
35
- ["badRoleElements", "=", 3],
36
- ["tagNames.SECTION.main", "=", 1],
37
- ["tagNames.SECTION.section", "=", 1],
38
- ["tagNames.H2.heading", "=", 1]
35
+ ["roleElements", "=", 9],
36
+ ["badRoleElements", "=", 6],
37
+ ["redundantRoleElements", "=", 3],
38
+ ["tagNames.SECTION.main.bad", "=", 1],
39
+ ["tagNames.SECTION.section.bad", "=", 1],
40
+ ["tagNames.H2.heading.bad", "=", 0],
41
+ ["tagNames.H2.heading.redundant", "=", 1],
42
+ ["tagNames.H3.heading.bad", "=", 2],
43
+ ["tagNames.H3.heading.redundant", "=", 1],
44
+ ["tagNames.INPUT.spinbutton.redundant", "=", 1],
45
+ ["tagNames.INPUT.textbox.bad", "=", 1],
46
+ ["tagNames.INPUT.textbox.redundant", "=", 0],
47
+ ["tagNames.INPUT.combobox.bad", "=", 1]
39
48
  ]
40
49
  }
41
50
  ]
@@ -9,10 +9,10 @@
9
9
  <body>
10
10
  <main>
11
11
  <h1>Page with deviant hover behavior</h1>
12
- <p>This page contains a link to <a href="https://en.wikipedia.org" onmouseover="document.getElementById('translucent').style.opacity = 1">information</a>, a <button type="button" onmouseover="document.getElementById('hiddenP').style.display = 'block'">button</button>, and a <label>text input <input type="text"></label>. All three elements can be hovered over.</p>
12
+ <p>This page contains a link to <a href="https://en.wikipedia.org" onmouseover="document.getElementById('translucent').style.opacity = 1">information</a> and a <button type="button" onmouseover="document.getElementById('hiddenP').style.display = 'block'">bothersome button</button>. Both elements can be hovered over.</p>
13
13
  <p id="hiddenP" style="display: none">The button, when hovered over, makes this paragraph, and therefore this <a href="https://en.wikipedia.org/wiki/Web_accessibility">link on web accessibility</a> and this <button type="button">new button</button>, visible.</p>
14
14
  <p id="translucent" style="opacity: 0.4">The first link, when hovered over, changes the opacity of this paragraph from 0.4 to 1. That indirectly changes the opacity of this <span>word</span>, too.</p>
15
- <p>The small button is mostly covered by a large one below.</p>
15
+ <p>The small button is mostly covered by a large one here, preventing the small button from receiving a hover event.</p>
16
16
  <p style="position: relative"><button style="position: absolute; left: 10rem">button</button><button style="position: absolute; left: 11rem; top: -0.5rem; font-size: x-large">bigger button</button></p>
17
17
  </main>
18
18
  </body>
@@ -9,7 +9,7 @@
9
9
  <body>
10
10
  <main>
11
11
  <h1>Page with standard hover behavior</h1>
12
- <p>This paragraph contains a link to <a href="https://en.wikipedia.org">information</a>, a <button type="button">button</button>, and a <label>text input <input type="text"></label>. All three can be hovered over, and hovering over any of them does not trigger any change in content.</p>
12
+ <p>This paragraph contains a link to <a href="https://en.wikipedia.org">information</a> and a <button type="button">button</button>. Both of them can be hovered over, and hovering over either of them does not trigger any change in content.</p>
13
13
  </main>
14
14
  </body>
15
15
  </html>
@@ -11,15 +11,37 @@
11
11
  <h1>Page with deviant role elements</h1>
12
12
  <section role="section">
13
13
  <h2>Abstraction</h2>
14
- <p>This section has an abstract role.</p>
14
+ <p>This section has an abstract role, so it is bad.</p>
15
15
  </section>
16
16
  <section>
17
17
  <h2 role="heading">Redundancy</h2>
18
- <p>This section has a redundant role.</p>
18
+ <p>The heading of this section has a redundant role, with an inferred level.</p>
19
+ <section>
20
+ <h3 role="heading" aria-level="3">Redundancy with explicit level</h3>
21
+ <p>The heading of this section has a redundant role because the stated level is 3 and the implicit level is 3.</p>
22
+ </section>
23
+ <section>
24
+ <h3 role="heading">Failed redundancy for missing level</h3>
25
+ <p>The heading of this section has a heading that fails redundancy because the inferred level is 2 but the explicit level is 3. So the element role is bad.</p>
26
+ </section>
27
+ <section>
28
+ <h3 role="heading" aria-level="4">Failed redundancy for wrong level</h3>
29
+ <p>The heading of this section has a heading that fails redundancy because the attributional level is 4 but the explicit level is 3. So the element role is bad.</p>
30
+ </section>
19
31
  </section>
20
32
  <section>
21
33
  <h2>Conflict</h2>
22
- <p>The parent section of these sections has an unnecessary explicit role, instead of an element with the same implicit role.</p>
34
+ <p>The parent section of these sections has an unnecessary explicit role <code>main</code>, instead of a <code>main</code> element with the same implicit role.</p>
35
+ </section>
36
+ <section>
37
+ <h2>Attributes</h2>
38
+ <h3>Valid redundancy</h3>
39
+ <p>This paragraph contains an input for a number. It has an implicit <code>spinbutton</code> role and the same redundant explicit role. <input type="number" role="spinbutton"></p>
40
+ <h3>Failed redundancy</h3>
41
+ <p>This paragraph contains an input for a number. It has an implicit <code>spinbutton</code> role but an explicit <code>textbox</code> role, so its role is bad. <input type="number" role="textbox"></p>
42
+ <h3>Attribute existence</h3>
43
+ <datalist id="options"><option value="a"></option><option value="b"></option></datalist>
44
+ <p>This input is identical, except that it omits the <code>list</code> attribute. That makes its implicit role <code>textbox</code>, so the explicit role of <code>combobox</code> is bad. <input role="combobox"></p>
23
45
  </section>
24
46
  </section>
25
47
  </body>