@markuplint/rules 3.8.0 → 3.10.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.
Files changed (103) hide show
  1. package/lib/attr-duplication/index.d.ts +1 -3
  2. package/lib/attr-value-quotes/index.d.ts +2 -2
  3. package/lib/case-sensitive-attr-name/index.d.ts +1 -1
  4. package/lib/case-sensitive-tag-name/index.d.ts +1 -1
  5. package/lib/character-reference/index.d.ts +1 -1
  6. package/lib/class-naming/index.d.ts +1 -1
  7. package/lib/create-message.d.ts +2 -12
  8. package/lib/deprecated-attr/index.d.ts +1 -3
  9. package/lib/deprecated-element/index.d.ts +1 -3
  10. package/lib/disallowed-element/index.d.ts +1 -1
  11. package/lib/doctype/index.d.ts +2 -2
  12. package/lib/end-tag/index.d.ts +1 -1
  13. package/lib/helpers.d.ts +18 -39
  14. package/lib/id-duplication/index.d.ts +1 -3
  15. package/lib/index.d.ts +7 -7
  16. package/lib/ineffective-attr/index.d.ts +1 -3
  17. package/lib/invalid-attr/index.d.ts +31 -38
  18. package/lib/label-has-control/index.d.ts +1 -3
  19. package/lib/landmark-roles/index.d.ts +3 -3
  20. package/lib/no-boolean-attr-value/index.d.ts +1 -3
  21. package/lib/no-default-value/index.d.ts +1 -3
  22. package/lib/no-empty-palpable-content/index.d.ts +3 -3
  23. package/lib/no-hard-code-id/index.d.ts +1 -3
  24. package/lib/no-refer-to-non-existent-id/index.d.ts +4 -9
  25. package/lib/no-use-event-handler-attr/index.d.ts +2 -2
  26. package/lib/permitted-contents/choice.d.ts +1 -7
  27. package/lib/permitted-contents/complex-branch.d.ts +1 -7
  28. package/lib/permitted-contents/count-pattern.d.ts +2 -17
  29. package/lib/permitted-contents/debug.browser.d.ts +15 -15
  30. package/lib/permitted-contents/debug.d.ts +1 -1
  31. package/lib/permitted-contents/index.d.ts +1 -1
  32. package/lib/permitted-contents/matches-selector.d.ts +1 -6
  33. package/lib/permitted-contents/order.d.ts +1 -7
  34. package/lib/permitted-contents/recursive-branch.d.ts +1 -7
  35. package/lib/permitted-contents/represent-transparent-nodes.d.ts +3 -7
  36. package/lib/permitted-contents/start.d.ts +1 -6
  37. package/lib/permitted-contents/types.d.ts +29 -29
  38. package/lib/permitted-contents/utils.d.ts +1 -1
  39. package/lib/placeholder-label-option/index.d.ts +1 -1
  40. package/lib/require-accessible-name/index.d.ts +2 -2
  41. package/lib/require-datetime/index.d.ts +2 -2
  42. package/lib/require-datetime/types.d.ts +2 -2
  43. package/lib/required-attr/index.d.ts +3 -3
  44. package/lib/required-element/index.d.ts +2 -2
  45. package/lib/required-h1/index.d.ts +3 -3
  46. package/lib/use-list/index.d.ts +2 -2
  47. package/lib/wai-aria/checkings/_.d.ts +12 -6
  48. package/lib/wai-aria/checkings/default-value.d.ts +3 -7
  49. package/lib/wai-aria/checkings/deprecated-props.d.ts +4 -8
  50. package/lib/wai-aria/checkings/implicit-props.d.ts +4 -8
  51. package/lib/wai-aria/checkings/no-global-prop.d.ts +3 -7
  52. package/lib/wai-aria/checkings/required-owned-elements.d.ts +3 -7
  53. package/lib/wai-aria/checkings/required-prop copy.d.ts +12 -6
  54. package/lib/wai-aria/checkings/required-prop.d.ts +6 -12
  55. package/lib/wai-aria/checkings/unadopted-explicit-roles.d.ts +12 -6
  56. package/lib/wai-aria/checkings/value.d.ts +6 -15
  57. package/lib/wai-aria/index.d.ts +1 -1
  58. package/lib/wai-aria/types.d.ts +10 -10
  59. package/lib/xxx/index.d.ts +2 -0
  60. package/lib/xxx/index.js +32 -0
  61. package/package.json +13 -15
  62. package/test/attr-check.spec.js +77 -0
  63. package/test/attr-duplication/index.spec.js +159 -0
  64. package/test/attr-value-quotes/index.spec.js +133 -0
  65. package/test/case-sensitive-attr-name/index.spec.js +65 -0
  66. package/test/case-sensitive-tag-name/index.spec.js +80 -0
  67. package/test/character-reference/index.spec.js +57 -0
  68. package/test/class-naming/index.spec.js +470 -0
  69. package/test/create-message.spec.js +497 -0
  70. package/test/deprecated-attr/index.spec.js +48 -0
  71. package/test/deprecated-element/index.spec.js +48 -0
  72. package/test/disallowed-element/index.spec.js +90 -0
  73. package/test/doctype/index.spec.js +50 -0
  74. package/test/end-tag/index.spec.js +162 -0
  75. package/test/id-duplication/index.spec.js +47 -0
  76. package/test/ineffective-attr/index.spec.js +31 -0
  77. package/test/invalid-attr/index.spec.js +1632 -0
  78. package/test/label-has-control/index.spec.js +29 -0
  79. package/test/landmark-roles/index.spec.js +187 -0
  80. package/test/no-boolean-attr-value/index.spec.js +41 -0
  81. package/test/no-default-value/index.spec.js +74 -0
  82. package/test/no-empty-palpable-content/index.spec.js +115 -0
  83. package/test/no-hard-code-id/index.spec.js +100 -0
  84. package/test/no-refer-to-non-existent-id/index.spec.js +254 -0
  85. package/test/no-use-event-handler-attr/index.spec.js +57 -0
  86. package/test/permitted-contents/choice.spec.js +174 -0
  87. package/test/permitted-contents/count-pattern.spec.js +308 -0
  88. package/test/permitted-contents/index.spec.js +1371 -0
  89. package/test/permitted-contents/matches-selector.spec.js +59 -0
  90. package/test/permitted-contents/order.spec.js +351 -0
  91. package/test/permitted-contents/recursive-branch.spec.js +41 -0
  92. package/test/permitted-contents/represent-transparent-nodes.spec.js +24 -0
  93. package/test/permitted-contents/start.spec.js +67 -0
  94. package/test/placeholder-label-option/index.spec.js +113 -0
  95. package/test/require-accessible-name/index.spec.js +446 -0
  96. package/test/require-datetime/index.spec.js +54 -0
  97. package/test/require-datetime/utils.spec.js +52 -0
  98. package/test/required-attr/index.spec.js +414 -0
  99. package/test/required-element/index.spec.js +188 -0
  100. package/test/required-h1/index.spec.js +69 -0
  101. package/test/use-list/index.spec.js +109 -0
  102. package/test/wai-aria/checkings/value.spec.js +33 -0
  103. package/test/wai-aria/index.spec.js +1173 -0
@@ -0,0 +1,1173 @@
1
+ const { mlRuleTest } = require('markuplint');
2
+
3
+ const rule = require('../../lib/wai-aria').default;
4
+
5
+ describe("Use the role that doesn't exist in the spec", () => {
6
+ test('[role=hoge]', async () => {
7
+ expect((await mlRuleTest(rule, '<div role="hoge"></div>')).violations).toStrictEqual([
8
+ {
9
+ severity: 'error',
10
+ line: 1,
11
+ col: 12,
12
+ message:
13
+ 'The "hoge" role does not exist according to the WAI-ARIA specification. This "hoge" role does not exist in WAI-ARIA.',
14
+ raw: 'hoge',
15
+ },
16
+ ]);
17
+
18
+ expect((await mlRuleTest(rule, '<div role="none hoge"></div>')).violations).toStrictEqual([
19
+ {
20
+ severity: 'error',
21
+ line: 1,
22
+ col: 17,
23
+ message:
24
+ 'The "hoge" role does not exist according to the WAI-ARIA specification. This "hoge" role does not exist in WAI-ARIA.',
25
+ raw: 'hoge',
26
+ },
27
+ ]);
28
+ });
29
+
30
+ test('Graphics ARIA to HTML', async () => {
31
+ expect((await mlRuleTest(rule, '<div role="graphics-document"></div>')).violations).toStrictEqual([
32
+ {
33
+ severity: 'error',
34
+ line: 1,
35
+ col: 12,
36
+ message:
37
+ 'The "graphics-document" role does not exist according to the WAI-ARIA specification. This "graphics-document" role does not exist in WAI-ARIA.',
38
+ raw: 'graphics-document',
39
+ },
40
+ ]);
41
+
42
+ expect((await mlRuleTest(rule, '<svg><rect role="graphics-document"></rect></svg>')).violations).toStrictEqual(
43
+ [],
44
+ );
45
+ });
46
+ });
47
+
48
+ describe('Use the abstract role', () => {
49
+ test('[role=roletype]', async () => {
50
+ expect((await mlRuleTest(rule, '<div role="roletype"></div>')).violations).toStrictEqual([
51
+ {
52
+ severity: 'error',
53
+ line: 1,
54
+ col: 12,
55
+ message: 'The "roletype" role is the abstract role',
56
+ raw: 'roletype',
57
+ },
58
+ ]);
59
+
60
+ expect((await mlRuleTest(rule, '<div role="article roletype"></div>')).violations).toStrictEqual([
61
+ {
62
+ severity: 'error',
63
+ line: 1,
64
+ col: 20,
65
+ message: 'The "roletype" role is the abstract role',
66
+ raw: 'roletype',
67
+ },
68
+ ]);
69
+ });
70
+ });
71
+
72
+ describe("Use the property/state that doesn't belong to a set role (or an implicit role)", () => {
73
+ test('[aria-checked=true]', async () => {
74
+ const { violations } = await mlRuleTest(rule, '<div aria-checked="true"></div>');
75
+
76
+ expect(violations).toStrictEqual([
77
+ {
78
+ severity: 'error',
79
+ line: 1,
80
+ col: 6,
81
+ message: 'The "aria-checked" ARIA state is disallowed on the "generic" role',
82
+ raw: 'aria-checked="true"',
83
+ },
84
+ ]);
85
+ });
86
+
87
+ test('[aria-checked=true]', async () => {
88
+ const { violations } = await mlRuleTest(rule, '<div aria-checked="true"></div>', {
89
+ rule: {
90
+ options: { version: '1.1' },
91
+ },
92
+ });
93
+
94
+ expect(violations).toStrictEqual([
95
+ {
96
+ severity: 'error',
97
+ line: 1,
98
+ col: 6,
99
+ message: 'The "aria-checked" ARIA state is not global state',
100
+ raw: 'aria-checked="true"',
101
+ },
102
+ ]);
103
+ });
104
+
105
+ test('button[aria-checked=true]', async () => {
106
+ const { violations } = await mlRuleTest(rule, '<button aria-checked="true"></button>');
107
+
108
+ expect(violations).toStrictEqual([
109
+ {
110
+ severity: 'error',
111
+ line: 1,
112
+ col: 9,
113
+ message: 'The "aria-checked" ARIA state is disallowed on the "button" role',
114
+ raw: 'aria-checked="true"',
115
+ },
116
+ ]);
117
+ });
118
+
119
+ test('button[aria-pressed=true]', async () => {
120
+ const { violations } = await mlRuleTest(rule, '<button aria-pressed="true"></button>');
121
+
122
+ expect(violations.length).toBe(0);
123
+ });
124
+ });
125
+
126
+ describe('Use an invalid value of the property/state', () => {
127
+ test('[aria-current=foo]', async () => {
128
+ const { violations } = await mlRuleTest(rule, '<div aria-current="foo"></div>');
129
+
130
+ expect(violations).toStrictEqual([
131
+ {
132
+ severity: 'error',
133
+ line: 1,
134
+ col: 6,
135
+ message:
136
+ 'The "foo" is disallowed on the "aria-current" ARIA state. Allowed values are: "page", "step", "location", "date", "time", "true", "false"',
137
+ raw: 'aria-current="foo"',
138
+ },
139
+ ]);
140
+ });
141
+
142
+ test('[aria-current=page]', async () => {
143
+ const { violations } = await mlRuleTest(rule, '<div aria-current="page"></div>');
144
+
145
+ expect(violations.length).toBe(0);
146
+ });
147
+
148
+ test('disabled', async () => {
149
+ const { violations } = await mlRuleTest(rule, '<div aria-current="foo"></div>', {
150
+ rule: {
151
+ options: {
152
+ checkingValue: false,
153
+ },
154
+ },
155
+ });
156
+
157
+ expect(violations.length).toBe(0);
158
+ });
159
+ });
160
+
161
+ describe('Use the not permitted role according to ARIA in HTML', () => {
162
+ test('script[role=link]', async () => {
163
+ const { violations } = await mlRuleTest(rule, '<script role="link"></script>');
164
+
165
+ expect(violations).toStrictEqual([
166
+ {
167
+ severity: 'error',
168
+ line: 1,
169
+ col: 9,
170
+ message: 'Cannot overwrite the role of the "script" element according to ARIA in HTML specification',
171
+ raw: 'role="link"',
172
+ },
173
+ ]);
174
+ });
175
+
176
+ test('a[role=document]', async () => {
177
+ const { violations } = await mlRuleTest(rule, '<a href role="document"></a>');
178
+
179
+ expect(violations).toStrictEqual([
180
+ {
181
+ severity: 'error',
182
+ line: 1,
183
+ col: 15,
184
+ message:
185
+ 'Cannot overwrite the "document" role to the "a" element according to ARIA in HTML specification',
186
+ raw: 'document',
187
+ },
188
+ ]);
189
+ });
190
+
191
+ test('disabled', async () => {
192
+ const { violations } = await mlRuleTest(rule, '<script role="link"></script>', {
193
+ rule: {
194
+ options: {
195
+ permittedAriaRoles: false,
196
+ },
197
+ },
198
+ });
199
+
200
+ expect(violations.length).toBe(0);
201
+ });
202
+ });
203
+
204
+ describe("Don't set the required property/state", () => {
205
+ test('heading needs aria-level', async () => {
206
+ const { violations } = await mlRuleTest(rule, '<div role="heading"></div>');
207
+
208
+ expect(violations).toStrictEqual([
209
+ {
210
+ severity: 'error',
211
+ line: 1,
212
+ col: 1,
213
+ message: 'Require the "aria-level" ARIA property on the "heading" role',
214
+ raw: '<div role="heading">',
215
+ },
216
+ ]);
217
+ });
218
+
219
+ test("h1 element doesn't needs aria-level", async () => {
220
+ const { violations } = await mlRuleTest(rule, '<h1></h1>');
221
+
222
+ expect(violations).toStrictEqual([]);
223
+ });
224
+ });
225
+
226
+ describe('Set the implicit role explicitly', () => {
227
+ test('a[href][role=link]', async () => {
228
+ const { violations } = await mlRuleTest(rule, '<a href="path/to" role="link"></a>');
229
+
230
+ expect(violations).toStrictEqual([
231
+ {
232
+ severity: 'error',
233
+ line: 1,
234
+ col: 25,
235
+ message: 'The "link" role is the implicit role of the "a" element',
236
+ raw: 'link',
237
+ },
238
+ ]);
239
+ });
240
+
241
+ test('header[role=banner]', async () => {
242
+ const { violations } = await mlRuleTest(rule, '<header role="banner"></header>');
243
+
244
+ expect(violations).toStrictEqual([
245
+ {
246
+ severity: 'error',
247
+ line: 1,
248
+ col: 15,
249
+ message: 'The "banner" role is the implicit role of the "header" element',
250
+ raw: 'banner',
251
+ },
252
+ ]);
253
+
254
+ const { violations: violations2 } = await mlRuleTest(
255
+ rule,
256
+ '<header role="banner"><article></article></header>',
257
+ );
258
+
259
+ expect(violations2).toStrictEqual([
260
+ {
261
+ severity: 'error',
262
+ line: 1,
263
+ col: 15,
264
+ message:
265
+ 'Cannot overwrite the "banner" role to the "header" element according to ARIA in HTML specification',
266
+ raw: 'banner',
267
+ },
268
+ ]);
269
+ });
270
+
271
+ test('disabled', async () => {
272
+ const { violations } = await mlRuleTest(rule, '<a href="path/to" role="link"></a>', {
273
+ rule: {
274
+ options: {
275
+ disallowSetImplicitRole: false,
276
+ },
277
+ },
278
+ });
279
+
280
+ expect(violations.length).toBe(0);
281
+ });
282
+ });
283
+
284
+ describe('Set the default value of the property/state explicitly', () => {
285
+ test('aria-live="off"', async () => {
286
+ const { violations } = await mlRuleTest(rule, '<div aria-live="off"></div>', {
287
+ rule: {
288
+ options: {
289
+ disallowDefaultValue: true,
290
+ },
291
+ },
292
+ });
293
+
294
+ expect(violations).toStrictEqual([
295
+ {
296
+ severity: 'error',
297
+ line: 1,
298
+ col: 6,
299
+ message: 'It is default value',
300
+ raw: 'aria-live="off"',
301
+ },
302
+ ]);
303
+ });
304
+ });
305
+
306
+ describe('Set the deprecated property/state', () => {
307
+ test('aria-disabled is deprecated in article', async () => {
308
+ const { violations } = await mlRuleTest(rule, '<article aria-disabled="true"></article>');
309
+
310
+ expect(violations).toStrictEqual([
311
+ {
312
+ severity: 'error',
313
+ line: 1,
314
+ col: 10,
315
+ message: 'The "aria-disabled" ARIA state is deprecated on the "article" role',
316
+ raw: 'aria-disabled="true"',
317
+ },
318
+ ]);
319
+ });
320
+
321
+ test('aria-disabled is deprecated in article role', async () => {
322
+ const { violations } = await mlRuleTest(rule, '<div role="article" aria-disabled="true"></div>');
323
+
324
+ expect(violations).toStrictEqual([
325
+ {
326
+ severity: 'error',
327
+ line: 1,
328
+ col: 21,
329
+ message: 'The "aria-disabled" ARIA state is deprecated on the "article" role',
330
+ raw: 'aria-disabled="true"',
331
+ },
332
+ ]);
333
+ });
334
+
335
+ test('disable', async () => {
336
+ const { violations } = await mlRuleTest(rule, '<article aria-disabled="true"></article>', {
337
+ rule: {
338
+ options: {
339
+ checkingDeprecatedProps: false,
340
+ },
341
+ },
342
+ });
343
+
344
+ expect(violations).toStrictEqual([]);
345
+ });
346
+ });
347
+
348
+ describe('Set the property/state explicitly when its element has semantic HTML attribute equivalent to it according to ARIA in HTML.', () => {
349
+ test('checked and aria-checked="true"', async () => {
350
+ const { violations } = await mlRuleTest(rule, '<input type="checkbox" checked aria-checked="true" />');
351
+
352
+ expect(violations).toStrictEqual([
353
+ {
354
+ severity: 'error',
355
+ line: 1,
356
+ col: 32,
357
+ message:
358
+ 'The "aria-checked" ARIA state must not use on the "input" element. Add the "checked" attribute if you use the ARIA state',
359
+ raw: 'aria-checked="true"',
360
+ },
361
+ {
362
+ severity: 'error',
363
+ line: 1,
364
+ col: 32,
365
+ message:
366
+ 'The "aria-checked" ARIA state has the same semantics as the current "checked" attribute or the implicit "checked" attribute',
367
+ raw: 'aria-checked="true"',
368
+ },
369
+ ]);
370
+ });
371
+
372
+ test('checked and aria-checked="false"', async () => {
373
+ const { violations } = await mlRuleTest(rule, '<input type="checkbox" checked aria-checked="false" />');
374
+
375
+ expect(violations).toStrictEqual([
376
+ {
377
+ severity: 'error',
378
+ line: 1,
379
+ col: 32,
380
+ message:
381
+ 'The "aria-checked" ARIA state must not use on the "input" element. Add the "checked" attribute if you use the ARIA state',
382
+ raw: 'aria-checked="false"',
383
+ },
384
+ {
385
+ severity: 'error',
386
+ line: 1,
387
+ col: 32,
388
+ message: 'The "aria-checked" ARIA state contradicts the current "checked" attribute',
389
+ raw: 'aria-checked="false"',
390
+ },
391
+ ]);
392
+ });
393
+
394
+ test('only aria-checked="true"', async () => {
395
+ const { violations } = await mlRuleTest(rule, '<input type="checkbox" aria-checked="true" />');
396
+
397
+ expect(violations).toStrictEqual([
398
+ {
399
+ severity: 'error',
400
+ line: 1,
401
+ col: 24,
402
+ message:
403
+ 'The "aria-checked" ARIA state must not use on the "input" element. Add the "checked" attribute if you use the ARIA state',
404
+ raw: 'aria-checked="true"',
405
+ },
406
+ {
407
+ severity: 'error',
408
+ line: 1,
409
+ col: 24,
410
+ message: 'The "aria-checked" ARIA state contradicts the implicit "checked" attribute',
411
+ raw: 'aria-checked="true"',
412
+ },
413
+ ]);
414
+ });
415
+
416
+ test('check and aria-checked="mixed"', async () => {
417
+ const { violations } = await mlRuleTest(rule, '<input type="checkbox" checked aria-checked="mixed" />');
418
+
419
+ expect(violations).toStrictEqual([
420
+ {
421
+ severity: 'error',
422
+ line: 1,
423
+ col: 32,
424
+ message:
425
+ 'The "aria-checked" ARIA state must not use on the "input" element. Add the "checked" attribute if you use the ARIA state',
426
+ raw: 'aria-checked="mixed"',
427
+ },
428
+ ]);
429
+ });
430
+
431
+ test('placeholder="type hints" and aria-placeholder="type hints"', async () => {
432
+ const { violations } = await mlRuleTest(
433
+ rule,
434
+ '<input type="text" placeholder="type hints" aria-placeholder="type hints" />',
435
+ );
436
+
437
+ expect(violations).toStrictEqual([
438
+ {
439
+ severity: 'error',
440
+ line: 1,
441
+ col: 45,
442
+ message:
443
+ 'The "aria-placeholder" ARIA property has the same semantics as the current "placeholder" attribute or the implicit "placeholder" attribute',
444
+ raw: 'aria-placeholder="type hints"',
445
+ },
446
+ ]);
447
+ });
448
+
449
+ test('placeholder="type hints" and aria-placeholder="different value"', async () => {
450
+ const { violations } = await mlRuleTest(
451
+ rule,
452
+ '<input type="text" placeholder="type hints" aria-placeholder="different value" />',
453
+ );
454
+
455
+ expect(violations).toStrictEqual([
456
+ {
457
+ severity: 'error',
458
+ line: 1,
459
+ col: 45,
460
+ message: 'The "aria-placeholder" ARIA property contradicts the current "placeholder" attribute',
461
+ raw: 'aria-placeholder="different value"',
462
+ },
463
+ ]);
464
+ });
465
+
466
+ test('hidden vs aria-hidden', async () => {
467
+ const { violations: violations1 } = await mlRuleTest(rule, '<div hidden></div>');
468
+ const { violations: violations2 } = await mlRuleTest(rule, '<div hidden aria-hidden="true"></div>');
469
+ const { violations: violations3 } = await mlRuleTest(rule, '<div hidden aria-hidden="false"></div>');
470
+ const { violations: violations4 } = await mlRuleTest(rule, '<div aria-hidden="true"></div>');
471
+ const { violations: violations5 } = await mlRuleTest(rule, '<div aria-hidden="false"></div>');
472
+
473
+ expect(violations1[0]?.message).toBe(undefined);
474
+ expect(violations2[0]?.message).toBe(
475
+ 'The "aria-hidden" ARIA state has the same semantics as the current "hidden" attribute or the implicit "hidden" attribute',
476
+ );
477
+ expect(violations3[0]?.message).toBe('The "aria-hidden" ARIA state contradicts the current "hidden" attribute');
478
+ expect(violations4[0]?.message).toBe(undefined);
479
+ expect(violations5[0]?.message).toBe(undefined);
480
+ });
481
+
482
+ test('disable', async () => {
483
+ const { violations } = await mlRuleTest(rule, '<input type="checkbox" checked aria-checked="true" />', {
484
+ rule: {
485
+ options: {
486
+ disallowSetImplicitProps: false,
487
+ },
488
+ },
489
+ });
490
+
491
+ expect(violations).toStrictEqual([
492
+ {
493
+ severity: 'error',
494
+ line: 1,
495
+ col: 32,
496
+ message:
497
+ 'The "aria-checked" ARIA state must not use on the "input" element. Add the "checked" attribute if you use the ARIA state',
498
+ raw: 'aria-checked="true"',
499
+ },
500
+ ]);
501
+ });
502
+ });
503
+
504
+ describe('Required Owned Elements', () => {
505
+ test('Empty content', async () => {
506
+ expect((await mlRuleTest(rule, '<div role="list"></div>')).violations).toStrictEqual([
507
+ {
508
+ severity: 'error',
509
+ line: 1,
510
+ col: 1,
511
+ message: 'Require the "listitem" role. Or, require aria-busy="true"',
512
+ raw: '<div role="list">',
513
+ },
514
+ ]);
515
+
516
+ expect((await mlRuleTest(rule, '<div role="list" aria-busy="true"></div>')).violations).toStrictEqual([]);
517
+ });
518
+
519
+ test('Empty content (Implicit role)', async () => {
520
+ expect((await mlRuleTest(rule, '<ul></ul>')).violations).toStrictEqual([
521
+ {
522
+ severity: 'error',
523
+ line: 1,
524
+ col: 1,
525
+ message: 'Require the "listitem" role. Or, require aria-busy="true"',
526
+ raw: '<ul>',
527
+ },
528
+ ]);
529
+
530
+ expect((await mlRuleTest(rule, '<ul> </ul>')).violations).toStrictEqual([
531
+ {
532
+ severity: 'error',
533
+ line: 1,
534
+ col: 1,
535
+ message: 'Require the "listitem" role. Or, require aria-busy="true"',
536
+ raw: '<ul>',
537
+ },
538
+ ]);
539
+
540
+ expect((await mlRuleTest(rule, '<ul aria-busy="true"></ul>')).violations).toStrictEqual([]);
541
+ });
542
+
543
+ test('Invalid contents', async () => {
544
+ expect((await mlRuleTest(rule, '<table><tbody><tr><td></td></tr></tbody></table>')).violations).toStrictEqual(
545
+ [],
546
+ );
547
+ expect((await mlRuleTest(rule, '<table><tr><td></td></tr></table>')).violations).toStrictEqual([]);
548
+ expect((await mlRuleTest(rule, '<table><tbody></tbody></table>')).violations).toStrictEqual([
549
+ {
550
+ severity: 'error',
551
+ line: 1,
552
+ col: 1,
553
+ message: 'The "table" role expects the roles: "row", "rowgroup > row"',
554
+ raw: '<table>',
555
+ },
556
+ {
557
+ severity: 'error',
558
+ line: 1,
559
+ col: 8,
560
+ message: 'Require the "row" role. Or, require aria-busy="true"',
561
+ raw: '<tbody>',
562
+ },
563
+ ]);
564
+ });
565
+
566
+ test('Invalid contents', async () => {
567
+ expect((await mlRuleTest(rule, '<ul><div></div></ul>')).violations).toStrictEqual([
568
+ {
569
+ severity: 'error',
570
+ line: 1,
571
+ col: 1,
572
+ message: 'The "list" role expects the "listitem" role',
573
+ raw: '<ul>',
574
+ },
575
+ ]);
576
+ });
577
+
578
+ test('Valid contents', async () => {
579
+ const jsx = {
580
+ parser: {
581
+ '.*': '@markuplint/jsx-parser',
582
+ },
583
+ };
584
+ expect((await mlRuleTest(rule, '<ul><li /></ul>', jsx)).violations).toStrictEqual([]);
585
+ expect((await mlRuleTest(rule, '<ul>\n<li /></ul>', jsx)).violations).toStrictEqual([]);
586
+ });
587
+
588
+ test('Preprocessor Block', async () => {
589
+ const jsx = {
590
+ parser: {
591
+ '.*': '@markuplint/jsx-parser',
592
+ },
593
+ };
594
+ expect((await mlRuleTest(rule, '<ul aria-busy="true">{foo}</ul>', jsx)).violations).toStrictEqual([]);
595
+ expect((await mlRuleTest(rule, '<ul>{foo}</ul>', jsx)).violations).toStrictEqual([]);
596
+ });
597
+
598
+ test('Owned element has Preprocessor Block', async () => {
599
+ expect(
600
+ (await mlRuleTest(rule, '<table><tbody><tr><td>foo</td></tr></tbody></table>')).violations,
601
+ ).toStrictEqual([]);
602
+
603
+ expect(
604
+ (
605
+ await mlRuleTest(rule, '<table><tbody>{list.map((item) => <tr><td>{item}</td></tr>)}</tbody></table>', {
606
+ parser: {
607
+ '.*': '@markuplint/jsx-parser',
608
+ },
609
+ })
610
+ ).violations,
611
+ ).toStrictEqual([]);
612
+ });
613
+
614
+ test('Omit <tbody>', async () => {
615
+ expect((await mlRuleTest(rule, '<table><tr><td>foo</td></tr></table>')).violations).toStrictEqual([]);
616
+ expect(
617
+ (await mlRuleTest(rule, '<table><tbody><tr><td>foo</td></tr></tbody></table>')).violations,
618
+ ).toStrictEqual([]);
619
+ });
620
+ });
621
+
622
+ describe('Presentational Children', () => {
623
+ const enable = { rule: { options: { checkingPresentationalChildren: true } } };
624
+ test('The role attribute in the button', async () => {
625
+ expect(
626
+ (await mlRuleTest(rule, '<button><div role="none">foo</div></button>', enable)).violations,
627
+ ).toStrictEqual([
628
+ {
629
+ severity: 'error',
630
+ line: 1,
631
+ col: 9,
632
+ message:
633
+ 'It may be ineffective because it has the "button" role as an ancestor that doesn\'t expose its descendants to the accessibility tree',
634
+ raw: '<div role="none">',
635
+ },
636
+ ]);
637
+ });
638
+
639
+ test('The aria-* attribute in the tab', async () => {
640
+ expect(
641
+ (
642
+ await mlRuleTest(
643
+ rule,
644
+ '<ul role="tablist"><li role="tab"><span aria-hidden="true">foo</span></li></ul>',
645
+ enable,
646
+ )
647
+ ).violations,
648
+ ).toStrictEqual([
649
+ {
650
+ severity: 'error',
651
+ line: 1,
652
+ col: 35,
653
+ message:
654
+ 'It may be ineffective because it has the "tab" role as an ancestor that doesn\'t expose its descendants to the accessibility tree',
655
+ raw: '<span aria-hidden="true">',
656
+ },
657
+ ]);
658
+ });
659
+ });
660
+
661
+ describe('Including Elements in the Accessibility Tree', () => {
662
+ const enable = { rule: { options: { checkingInteractionInHidden: true } } };
663
+ test('Parent has aria-hidden', async () => {
664
+ expect(
665
+ (await mlRuleTest(rule, '<div aria-hidden="true"><button>foo</button></div>', enable)).violations,
666
+ ).toStrictEqual([
667
+ {
668
+ severity: 'error',
669
+ line: 1,
670
+ col: 25,
671
+ message: 'It may be focusable in spite of it has the ancestor that has aria-hidden=true',
672
+ raw: '<button>',
673
+ },
674
+ ]);
675
+ });
676
+
677
+ test('Ancestor has aria-hidden', async () => {
678
+ expect(
679
+ (await mlRuleTest(rule, '<div aria-hidden="true"><span><button>foo</button></span></div>', enable))
680
+ .violations,
681
+ ).toStrictEqual([
682
+ {
683
+ severity: 'error',
684
+ line: 1,
685
+ col: 31,
686
+ message: 'It may be focusable in spite of it has the ancestor that has aria-hidden=true',
687
+ raw: '<button>',
688
+ },
689
+ ]);
690
+ });
691
+
692
+ test('Has aria-hidden', async () => {
693
+ expect(
694
+ (await mlRuleTest(rule, '<div><span><button aria-hidden="true">foo</button></span></div>', enable))
695
+ .violations,
696
+ ).toStrictEqual([
697
+ {
698
+ severity: 'error',
699
+ line: 1,
700
+ col: 12,
701
+ message: 'It may be focusable in spite of it has aria-hidden=true',
702
+ raw: '<button aria-hidden="true">',
703
+ },
704
+ ]);
705
+ });
706
+ });
707
+
708
+ describe('childNodeRules', () => {
709
+ test('ex. For Safari + VoiceOver', async () => {
710
+ const { violations } = await mlRuleTest(rule, '<img src="path/to.svg" alt="text" role="img" />', {
711
+ nodeRule: [
712
+ {
713
+ selector: 'img[src$=.svg]',
714
+ rule: {
715
+ options: {
716
+ disallowSetImplicitRole: false,
717
+ },
718
+ },
719
+ },
720
+ ],
721
+ });
722
+
723
+ expect(violations.length).toBe(0);
724
+ });
725
+ });
726
+
727
+ describe('Pretenders Option', () => {
728
+ test('list > listitem', async () => {
729
+ expect(
730
+ (
731
+ await mlRuleTest(rule, '<ul><Item>item</Item></ul>', {
732
+ parser: {
733
+ '.*': '@markuplint/jsx-parser',
734
+ },
735
+ })
736
+ ).violations,
737
+ ).toStrictEqual([
738
+ {
739
+ severity: 'error',
740
+ line: 1,
741
+ col: 1,
742
+ message: 'The "list" role expects the "listitem" role',
743
+ raw: '<ul>',
744
+ },
745
+ ]);
746
+ expect(
747
+ (
748
+ await mlRuleTest(rule, '<ul><Item>item</Item></ul>', {
749
+ parser: {
750
+ '.*': '@markuplint/jsx-parser',
751
+ },
752
+ pretenders: [
753
+ {
754
+ selector: 'Item',
755
+ as: 'li',
756
+ },
757
+ ],
758
+ })
759
+ ).violations,
760
+ ).toStrictEqual([]);
761
+ expect(
762
+ (
763
+ await mlRuleTest(rule, '<ul><Item>item</Item></ul>', {
764
+ parser: {
765
+ '.*': '@markuplint/jsx-parser',
766
+ },
767
+ pretenders: [
768
+ {
769
+ selector: 'Item',
770
+ as: {
771
+ element: 'div',
772
+ attrs: [
773
+ {
774
+ name: 'role',
775
+ value: 'listitem',
776
+ },
777
+ ],
778
+ },
779
+ },
780
+ ],
781
+ })
782
+ ).violations,
783
+ ).toStrictEqual([]);
784
+ expect(
785
+ (
786
+ await mlRuleTest(rule, '<ul><Item role="listitem">item</Item></ul>', {
787
+ parser: {
788
+ '.*': '@markuplint/jsx-parser',
789
+ },
790
+ })
791
+ ).violations,
792
+ ).toStrictEqual([
793
+ {
794
+ severity: 'error',
795
+ line: 1,
796
+ col: 1,
797
+ message: 'The "list" role expects the "listitem" role',
798
+ raw: '<ul>',
799
+ },
800
+ ]);
801
+ expect(
802
+ (
803
+ await mlRuleTest(rule, '<ul><Item role="listitem">item</Item></ul>', {
804
+ parser: {
805
+ '.*': '@markuplint/jsx-parser',
806
+ },
807
+ pretenders: [
808
+ {
809
+ selector: 'Item',
810
+ as: {
811
+ element: 'div',
812
+ },
813
+ },
814
+ ],
815
+ })
816
+ ).violations,
817
+ ).toStrictEqual([
818
+ {
819
+ severity: 'error',
820
+ line: 1,
821
+ col: 1,
822
+ message: 'The "list" role expects the "listitem" role',
823
+ raw: '<ul>',
824
+ },
825
+ ]);
826
+ expect(
827
+ (
828
+ await mlRuleTest(rule, '<ul><Item role="listitem">item</Item></ul>', {
829
+ parser: {
830
+ '.*': '@markuplint/jsx-parser',
831
+ },
832
+ pretenders: [
833
+ {
834
+ selector: 'Item',
835
+ as: {
836
+ element: 'div',
837
+ inheritAttrs: true,
838
+ },
839
+ },
840
+ ],
841
+ })
842
+ ).violations,
843
+ ).toStrictEqual([]);
844
+ expect(
845
+ (
846
+ await mlRuleTest(rule, '<ul><Item role="listitem">item</Item></ul>', {
847
+ parser: {
848
+ '.*': '@markuplint/jsx-parser',
849
+ },
850
+ pretenders: [
851
+ {
852
+ selector: 'Item',
853
+ as: {
854
+ element: 'div',
855
+ attrs: [
856
+ {
857
+ name: 'role',
858
+ },
859
+ ],
860
+ },
861
+ },
862
+ ],
863
+ })
864
+ ).violations,
865
+ ).toStrictEqual([
866
+ {
867
+ severity: 'error',
868
+ line: 1,
869
+ col: 1,
870
+ message: 'The "list" role expects the "listitem" role',
871
+ raw: '<ul>',
872
+ },
873
+ ]);
874
+ expect(
875
+ (
876
+ await mlRuleTest(rule, '<ul><Item data-role="listitem">item</Item></ul>', {
877
+ parser: {
878
+ '.*': '@markuplint/jsx-parser',
879
+ },
880
+ pretenders: [
881
+ {
882
+ selector: 'Item',
883
+ as: {
884
+ element: 'div',
885
+ attrs: [
886
+ {
887
+ name: 'role',
888
+ value: {
889
+ fromAttr: 'data-role',
890
+ },
891
+ },
892
+ ],
893
+ },
894
+ },
895
+ ],
896
+ })
897
+ ).violations,
898
+ ).toStrictEqual([]);
899
+ });
900
+ });
901
+
902
+ test('Booleanish', async () => {
903
+ expect((await mlRuleTest(rule, '<div aria-hidden></div>')).violations).toStrictEqual([
904
+ {
905
+ severity: 'error',
906
+ line: 1,
907
+ col: 6,
908
+ message: 'The "" is disallowed on the "aria-hidden" ARIA state',
909
+ raw: 'aria-hidden',
910
+ },
911
+ ]);
912
+
913
+ expect(
914
+ (
915
+ await mlRuleTest(rule, '<div aria-hidden></div>', {
916
+ parser: {
917
+ '.*': '@markuplint/jsx-parser',
918
+ },
919
+ })
920
+ ).violations,
921
+ ).toStrictEqual([]);
922
+
923
+ expect(
924
+ (
925
+ await mlRuleTest(rule, '<div aria-hidden="invalid"></div>', {
926
+ parser: {
927
+ '.*': '@markuplint/jsx-parser',
928
+ },
929
+ })
930
+ ).violations,
931
+ ).toStrictEqual([
932
+ {
933
+ severity: 'error',
934
+ line: 1,
935
+ col: 6,
936
+ message: 'The "invalid" is disallowed on the "aria-hidden" ARIA state',
937
+ raw: 'aria-hidden="invalid"',
938
+ },
939
+ ]);
940
+ });
941
+
942
+ describe('Disallowed prop each element', () => {
943
+ test('disabled link', async () => {
944
+ const { violations } = await mlRuleTest(rule, '<a href="path/to" aria-disabled="true">disabled link</a>');
945
+ expect(violations).toStrictEqual([
946
+ {
947
+ severity: 'error',
948
+ line: 1,
949
+ col: 19,
950
+ message:
951
+ 'The "aria-disabled" ARIA state is not recommended to use on the "a" element. Remove the "href" attribute if you use the ARIA state',
952
+ raw: 'aria-disabled="true"',
953
+ },
954
+ ]);
955
+ });
956
+
957
+ // https://github.com/markuplint/markuplint/issues/745
958
+ test('#745 Updated spec', async () => {
959
+ const { violations } = await mlRuleTest(rule, '<html><body aria-hidden="true"></body></html>');
960
+ expect(violations).toStrictEqual([
961
+ {
962
+ severity: 'error',
963
+ line: 1,
964
+ col: 13,
965
+ message: 'The "aria-hidden" ARIA state must not use on the "body" element',
966
+ raw: 'aria-hidden="true"',
967
+ },
968
+ ]);
969
+ });
970
+ });
971
+
972
+ describe('Issues', () => {
973
+ // https://github.com/markuplint/markuplint/issues/397
974
+ // And https://github.com/markuplint/markuplint/issues/397#issuecomment-1148349418
975
+ // And https://github.com/markuplint/markuplint/issues/397#issuecomment-1156728358
976
+ test('#397', async () => {
977
+ {
978
+ const { violations } = await mlRuleTest(rule, '<table><tr><th aria-sort="ascending"></th></tr></table>');
979
+ expect(violations).toStrictEqual([
980
+ // https://github.com/markuplint/markuplint/issues/397#issuecomment-1156728358
981
+ // The element role is not `cell`.
982
+ // {
983
+ // severity: 'error',
984
+ // line: 1,
985
+ // col: 16,
986
+ // message: 'The "aria-sort" ARIA state/property is disallowed on the "cell" role',
987
+ // raw: 'aria-sort="ascending"',
988
+ // },
989
+ ]);
990
+ }
991
+
992
+ {
993
+ const { violations } = await mlRuleTest(
994
+ rule,
995
+ '<table><tr><th scope="row" aria-sort="ascending"></th></tr></table>',
996
+ );
997
+ expect(violations).toStrictEqual([]);
998
+ }
999
+ });
1000
+
1001
+ test('#606', async () => {
1002
+ expect(
1003
+ (
1004
+ await mlRuleTest(
1005
+ rule,
1006
+ `<ul>
1007
+ <template>
1008
+ <li></li>
1009
+ </template>
1010
+ </ul>`,
1011
+ )
1012
+ ).violations,
1013
+ ).toStrictEqual([
1014
+ {
1015
+ severity: 'error',
1016
+ line: 1,
1017
+ col: 1,
1018
+ message: 'Require the "listitem" role. Or, require aria-busy="true"',
1019
+ raw: '<ul>',
1020
+ },
1021
+ ]);
1022
+ expect(
1023
+ (
1024
+ await mlRuleTest(
1025
+ rule,
1026
+ `<ul aria-busy="true">
1027
+ <template>
1028
+ <li></li>
1029
+ </template>
1030
+ </ul>`,
1031
+ )
1032
+ ).violations,
1033
+ ).toStrictEqual([]);
1034
+
1035
+ expect(
1036
+ (
1037
+ await mlRuleTest(
1038
+ rule,
1039
+ `<ul>
1040
+ <!-- -->
1041
+ </ul>`,
1042
+ )
1043
+ ).violations,
1044
+ ).toStrictEqual([
1045
+ {
1046
+ severity: 'error',
1047
+ line: 1,
1048
+ col: 1,
1049
+ message: 'Require the "listitem" role. Or, require aria-busy="true"',
1050
+ raw: '<ul>',
1051
+ },
1052
+ ]);
1053
+ expect(
1054
+ (
1055
+ await mlRuleTest(
1056
+ rule,
1057
+ `<ul aria-busy="true">
1058
+ <!-- -->
1059
+ </ul>`,
1060
+ )
1061
+ ).violations,
1062
+ ).toStrictEqual([]);
1063
+
1064
+ expect(
1065
+ (
1066
+ await mlRuleTest(
1067
+ rule,
1068
+ `<table>
1069
+ <caption>
1070
+ text
1071
+ </caption>
1072
+ <tbody>
1073
+ <template>
1074
+ <tr>
1075
+ <td></td>
1076
+ </tr>
1077
+ </template>
1078
+ </tbody>
1079
+ </table>`,
1080
+ )
1081
+ ).violations,
1082
+ ).toStrictEqual([
1083
+ {
1084
+ severity: 'error',
1085
+ line: 1,
1086
+ col: 1,
1087
+ message: 'The "table" role expects the roles: "row", "rowgroup > row"',
1088
+ raw: '<table>',
1089
+ },
1090
+ {
1091
+ severity: 'error',
1092
+ line: 5,
1093
+ col: 7,
1094
+ message: 'Require the "row" role. Or, require aria-busy="true"',
1095
+ raw: '<tbody>',
1096
+ },
1097
+ ]);
1098
+ expect(
1099
+ (
1100
+ await mlRuleTest(
1101
+ rule,
1102
+ `<table>
1103
+ <caption>
1104
+ text
1105
+ </caption>
1106
+ <tbody>
1107
+ <tr>
1108
+ <td></td>
1109
+ </tr>
1110
+ </tbody>
1111
+ </table>`,
1112
+ )
1113
+ ).violations,
1114
+ ).toStrictEqual([]);
1115
+ expect(
1116
+ (
1117
+ await mlRuleTest(
1118
+ rule,
1119
+ `<table>
1120
+ <caption>
1121
+ text
1122
+ </caption>
1123
+ <tbody aria-busy="true">
1124
+ <template>
1125
+ <tr>
1126
+ <td></td>
1127
+ </tr>
1128
+ </template>
1129
+ </tbody>
1130
+ </table>`,
1131
+ )
1132
+ ).violations,
1133
+ ).toStrictEqual([]);
1134
+ });
1135
+
1136
+ test('#778', async () => {
1137
+ const jsx = {
1138
+ parser: {
1139
+ '.*': '@markuplint/jsx-parser',
1140
+ },
1141
+ };
1142
+
1143
+ const sourceCode = '<td role="gridcell" aria-selected="true"></td>';
1144
+ expect((await mlRuleTest(rule, sourceCode)).violations).toStrictEqual([]);
1145
+ expect((await mlRuleTest(rule, sourceCode, jsx)).violations).toStrictEqual([]);
1146
+
1147
+ const sourceCode2 = '<div role="rowgroup"><div role="row"><div role="gridcell"></div></div></div>';
1148
+ expect((await mlRuleTest(rule, sourceCode2)).violations).toStrictEqual([]);
1149
+ expect((await mlRuleTest(rule, sourceCode2, jsx)).violations).toStrictEqual([]);
1150
+
1151
+ const sourceCode3 = '<table><tbody><tr><td role="gridcell"></td></tr></tbody></table>';
1152
+ expect((await mlRuleTest(rule, sourceCode3)).violations).toStrictEqual([
1153
+ {
1154
+ severity: 'error',
1155
+ line: 1,
1156
+ col: 29,
1157
+ message:
1158
+ 'Cannot overwrite the "gridcell" role to the "td" element according to ARIA in HTML specification',
1159
+ raw: 'gridcell',
1160
+ },
1161
+ ]);
1162
+ expect((await mlRuleTest(rule, sourceCode3, jsx)).violations).toStrictEqual([
1163
+ {
1164
+ severity: 'error',
1165
+ line: 1,
1166
+ col: 29,
1167
+ message:
1168
+ 'Cannot overwrite the "gridcell" role to the "td" element according to ARIA in HTML specification',
1169
+ raw: 'gridcell',
1170
+ },
1171
+ ]);
1172
+ });
1173
+ });