|
18 | 18 | // Rule under test: |
19 | 19 | // - lib/rules/template-no-noninteractive-element-to-interactive-role.js |
20 | 20 | // (feat/template-no-noninteractive-to-interactive-role, PR #21). |
21 | | -// Non-interactive tag set is derived from axobject-query: any element |
22 | | -// whose AXObject mapping is exclusively {window, structure} with no |
23 | | -// attribute constraints. Interactive role set is shared with the sibling |
24 | | -// rule via lib/utils/interactive-roles.js (refactored in PR #27 to |
25 | | -// descend from the `widget` superclass in aria-query, plus `toolbar`). |
| 21 | +// Non-interactive tag set is derived by unioning aria-query's |
| 22 | +// `elementRoles` (tags mapping only to non-interactive, non-`generic` |
| 23 | +// roles) with axobject-query's `elementAXObjects` (unconstrained tags |
| 24 | +// whose AXObjects are exclusively `window`/`structure`). `header` is |
| 25 | +// excluded because its role depends on ancestry. Interactive role set |
| 26 | +// is shared with the sibling rule via lib/utils/interactive-roles.js |
| 27 | +// (refactored in PR #27 to descend from the `widget` superclass in |
| 28 | +// aria-query, plus `toolbar`). This is the same approach jsx-a11y takes |
| 29 | +// in src/util/isNonInteractiveElement.js. |
26 | 30 | // |
27 | | -// jsx-a11y, by contrast, uses a manually curated element→role map |
28 | | -// (src/util/implicitRoles + nonInteractiveMap). The two derivations diverge |
29 | | -// on a number of tags; those divergences are captured below. jsx-a11y also |
30 | | -// ships :recommended and :strict variants — our rule has no options and |
31 | | -// behaves close to jsx-a11y :strict (always flags everything), with the |
| 31 | +// jsx-a11y ships :recommended and :strict variants — our rule has no options |
| 32 | +// and behaves close to jsx-a11y :strict (always flags everything), with the |
32 | 33 | // exceptions documented below. |
33 | 34 |
|
34 | 35 | 'use strict'; |
@@ -242,50 +243,12 @@ ruleTester.run('audit:no-noninteractive-element-to-interactive-role (gts)', rule |
242 | 243 | // `<h1 role="BUTTON" />` is FLAGGED by our rule. Captured in invalid |
243 | 244 | // list — this is a divergence (stricter than jsx-a11y). |
244 | 245 |
|
245 | | - // === DIVERGENCE — section has no implicit role per jsx-a11y's map === |
246 | | - // jsx-a11y: `<section role="button" aria-label="...">` is INVALID in jsx-a11y |
247 | | - // (section is in nonInteractiveMap). Test exists in neverValid. |
248 | | - // Ours: `section` is NOT in our NON_INTERACTIVE_TAGS. axobject-query's |
249 | | - // `<section>` mapping is attribute-conditional (becomes `region` only with |
250 | | - // an accessible name), so our unconstrained-attributes filter excludes it. |
251 | | - // FALSE NEGATIVE. Captured below. |
252 | | - '<template><section role="button" aria-label="Aardvark"></section></template>', |
253 | | - |
254 | | - // === DIVERGENCE — misc tags jsx-a11y flags that we don't === |
255 | | - // axobject-query assigns these tags attribute-constrained AXObjects, so |
256 | | - // our "unconstrained non-interactive" filter drops them. jsx-a11y's |
257 | | - // nonInteractiveMap is hand-curated and includes them. Our rule does NOT |
258 | | - // flag the following; jsx-a11y DOES: |
259 | | - // |
260 | | - // address, aside, code, del, em, fieldset, hr, html, ins, optgroup, |
261 | | - // output, strong, sub, sup, tbody, tfoot, thead |
262 | | - // |
263 | | - // These are FALSE NEGATIVES (we are looser than jsx-a11y). |
264 | | - '<template><address role="button"></address></template>', |
265 | | - '<template><aside role="button"></aside></template>', |
266 | | - '<template><code role="button"></code></template>', |
267 | | - '<template><del role="button"></del></template>', |
268 | | - '<template><em role="button"></em></template>', |
269 | | - '<template><fieldset role="button"></fieldset></template>', |
270 | | - '<template><hr role="button" /></template>', |
271 | | - '<template><html role="button"></html></template>', |
272 | | - '<template><ins role="button"></ins></template>', |
273 | | - '<template><optgroup role="button"></optgroup></template>', |
274 | | - '<template><output role="button"></output></template>', |
275 | | - '<template><strong role="button"></strong></template>', |
276 | | - '<template><sub role="button"></sub></template>', |
277 | | - '<template><sup role="button"></sup></template>', |
278 | | - '<template><tbody role="button"></tbody></template>', |
279 | | - '<template><tfoot role="button"></tfoot></template>', |
280 | | - '<template><thead role="button"></thead></template>', |
281 | | - |
282 | | - // jsx-a11y flags <dd role="menuitem"> but we flag also (parity, in invalid). |
283 | | - // jsx-a11y flags the following but we DO NOT (same false-negative family): |
284 | | - '<template><fieldset role="menuitem"></fieldset></template>', |
285 | | - '<template><hr role="menuitem" /></template>', |
286 | | - '<template><tbody role="menuitem"></tbody></template>', |
287 | | - '<template><tfoot role="menuitem"></tfoot></template>', |
288 | | - '<template><thead role="menuitem"></thead></template>', |
| 246 | + // Previously-documented FALSE NEGATIVES for `section`, `address`, `aside`, |
| 247 | + // `code`, `del`, `em`, `fieldset`, `hr`, `html`, `ins`, `optgroup`, |
| 248 | + // `output`, `strong`, `sub`, `sup`, `tbody`, `tfoot`, `thead` were |
| 249 | + // resolved by augmenting the tag derivation with aria-query's elementRoles |
| 250 | + // (jsx-a11y does the same in isNonInteractiveElement). Those cases have |
| 251 | + // moved to the invalid list below. |
289 | 252 | ], |
290 | 253 |
|
291 | 254 | invalid: [ |
@@ -575,6 +538,128 @@ ruleTester.run('audit:no-noninteractive-element-to-interactive-role (gts)', rule |
575 | 538 | errors: [{ messageId: 'mismatch' }], |
576 | 539 | }, |
577 | 540 |
|
| 541 | + // === Upstream parity — HTML-AAM non-interactive tags captured via the |
| 542 | + // aria-query `elementRoles` augmentation (section → region, fieldset → |
| 543 | + // group, code/em/strong → code/emphasis/strong, tbody/tfoot/thead → |
| 544 | + // rowgroup, etc.). Prior to the elementRoles derivation these were false |
| 545 | + // negatives relative to jsx-a11y; the rule now flags them. === |
| 546 | + { |
| 547 | + code: '<template><section role="button" aria-label="Aardvark"></section></template>', |
| 548 | + output: null, |
| 549 | + errors: [{ messageId: 'mismatch' }], |
| 550 | + }, |
| 551 | + { |
| 552 | + code: '<template><address role="button"></address></template>', |
| 553 | + output: null, |
| 554 | + errors: [{ messageId: 'mismatch' }], |
| 555 | + }, |
| 556 | + { |
| 557 | + code: '<template><aside role="button"></aside></template>', |
| 558 | + output: null, |
| 559 | + errors: [{ messageId: 'mismatch' }], |
| 560 | + }, |
| 561 | + { |
| 562 | + code: '<template><code role="button"></code></template>', |
| 563 | + output: null, |
| 564 | + errors: [{ messageId: 'mismatch' }], |
| 565 | + }, |
| 566 | + { |
| 567 | + code: '<template><del role="button"></del></template>', |
| 568 | + output: null, |
| 569 | + errors: [{ messageId: 'mismatch' }], |
| 570 | + }, |
| 571 | + { |
| 572 | + code: '<template><em role="button"></em></template>', |
| 573 | + output: null, |
| 574 | + errors: [{ messageId: 'mismatch' }], |
| 575 | + }, |
| 576 | + { |
| 577 | + code: '<template><fieldset role="button"></fieldset></template>', |
| 578 | + output: null, |
| 579 | + errors: [{ messageId: 'mismatch' }], |
| 580 | + }, |
| 581 | + { |
| 582 | + code: '<template><hr role="button" /></template>', |
| 583 | + output: null, |
| 584 | + errors: [{ messageId: 'mismatch' }], |
| 585 | + }, |
| 586 | + { |
| 587 | + code: '<template><html role="button"></html></template>', |
| 588 | + output: null, |
| 589 | + errors: [{ messageId: 'mismatch' }], |
| 590 | + }, |
| 591 | + { |
| 592 | + code: '<template><ins role="button"></ins></template>', |
| 593 | + output: null, |
| 594 | + errors: [{ messageId: 'mismatch' }], |
| 595 | + }, |
| 596 | + { |
| 597 | + code: '<template><optgroup role="button"></optgroup></template>', |
| 598 | + output: null, |
| 599 | + errors: [{ messageId: 'mismatch' }], |
| 600 | + }, |
| 601 | + { |
| 602 | + code: '<template><output role="button"></output></template>', |
| 603 | + output: null, |
| 604 | + errors: [{ messageId: 'mismatch' }], |
| 605 | + }, |
| 606 | + { |
| 607 | + code: '<template><strong role="button"></strong></template>', |
| 608 | + output: null, |
| 609 | + errors: [{ messageId: 'mismatch' }], |
| 610 | + }, |
| 611 | + { |
| 612 | + code: '<template><sub role="button"></sub></template>', |
| 613 | + output: null, |
| 614 | + errors: [{ messageId: 'mismatch' }], |
| 615 | + }, |
| 616 | + { |
| 617 | + code: '<template><sup role="button"></sup></template>', |
| 618 | + output: null, |
| 619 | + errors: [{ messageId: 'mismatch' }], |
| 620 | + }, |
| 621 | + { |
| 622 | + code: '<template><tbody role="button"></tbody></template>', |
| 623 | + output: null, |
| 624 | + errors: [{ messageId: 'mismatch' }], |
| 625 | + }, |
| 626 | + { |
| 627 | + code: '<template><tfoot role="button"></tfoot></template>', |
| 628 | + output: null, |
| 629 | + errors: [{ messageId: 'mismatch' }], |
| 630 | + }, |
| 631 | + { |
| 632 | + code: '<template><thead role="button"></thead></template>', |
| 633 | + output: null, |
| 634 | + errors: [{ messageId: 'mismatch' }], |
| 635 | + }, |
| 636 | + // jsx-a11y also flags role="menuitem" on several of the above. |
| 637 | + { |
| 638 | + code: '<template><fieldset role="menuitem"></fieldset></template>', |
| 639 | + output: null, |
| 640 | + errors: [{ messageId: 'mismatch' }], |
| 641 | + }, |
| 642 | + { |
| 643 | + code: '<template><hr role="menuitem" /></template>', |
| 644 | + output: null, |
| 645 | + errors: [{ messageId: 'mismatch' }], |
| 646 | + }, |
| 647 | + { |
| 648 | + code: '<template><tbody role="menuitem"></tbody></template>', |
| 649 | + output: null, |
| 650 | + errors: [{ messageId: 'mismatch' }], |
| 651 | + }, |
| 652 | + { |
| 653 | + code: '<template><tfoot role="menuitem"></tfoot></template>', |
| 654 | + output: null, |
| 655 | + errors: [{ messageId: 'mismatch' }], |
| 656 | + }, |
| 657 | + { |
| 658 | + code: '<template><thead role="menuitem"></thead></template>', |
| 659 | + output: null, |
| 660 | + errors: [{ messageId: 'mismatch' }], |
| 661 | + }, |
| 662 | + |
578 | 663 | // === DIVERGENCE — jsx-a11y :recommended config exceptions === |
579 | 664 | // jsx-a11y :recommended has an allowedInvalidRoles config so the following |
580 | 665 | // all pass :recommended but FAIL :strict: |
@@ -779,26 +864,6 @@ hbsRuleTester.run('audit:no-noninteractive-element-to-interactive-role (hbs)', r |
779 | 864 | '<h1 role={{this.role}}></h1>', |
780 | 865 | // Empty role — ours skip; jsx-a11y doesn't cover. |
781 | 866 | '<h1 role=""></h1>', |
782 | | - |
783 | | - // DIVERGENCE: jsx-a11y flags these (per nonInteractiveMap); ours do not. |
784 | | - '<section role="button" aria-label="A"></section>', |
785 | | - '<address role="button"></address>', |
786 | | - '<aside role="button"></aside>', |
787 | | - '<fieldset role="button"></fieldset>', |
788 | | - '<code role="button"></code>', |
789 | | - '<em role="button"></em>', |
790 | | - '<strong role="button"></strong>', |
791 | | - '<tbody role="button"></tbody>', |
792 | | - '<tfoot role="button"></tfoot>', |
793 | | - '<thead role="button"></thead>', |
794 | | - '<hr role="button" />', |
795 | | - '<html role="button"></html>', |
796 | | - '<del role="button"></del>', |
797 | | - '<ins role="button"></ins>', |
798 | | - '<optgroup role="button"></optgroup>', |
799 | | - '<output role="button"></output>', |
800 | | - '<sub role="button"></sub>', |
801 | | - '<sup role="button"></sup>', |
802 | 867 | ], |
803 | 868 | invalid: [ |
804 | 869 | // Upstream parity — non-interactive + interactive role. |
@@ -833,6 +898,39 @@ hbsRuleTester.run('audit:no-noninteractive-element-to-interactive-role (hbs)', r |
833 | 898 | errors: [{ messageId: 'mismatch' }], |
834 | 899 | }, |
835 | 900 |
|
| 901 | + // Upstream parity — HTML-AAM non-interactive tags picked up via the |
| 902 | + // aria-query elementRoles augmentation. |
| 903 | + { |
| 904 | + code: '<section role="button" aria-label="A"></section>', |
| 905 | + output: null, |
| 906 | + errors: [{ messageId: 'mismatch' }], |
| 907 | + }, |
| 908 | + { |
| 909 | + code: '<fieldset role="button"></fieldset>', |
| 910 | + output: null, |
| 911 | + errors: [{ messageId: 'mismatch' }], |
| 912 | + }, |
| 913 | + { |
| 914 | + code: '<aside role="button"></aside>', |
| 915 | + output: null, |
| 916 | + errors: [{ messageId: 'mismatch' }], |
| 917 | + }, |
| 918 | + { |
| 919 | + code: '<hr role="button" />', |
| 920 | + output: null, |
| 921 | + errors: [{ messageId: 'mismatch' }], |
| 922 | + }, |
| 923 | + { |
| 924 | + code: '<strong role="button"></strong>', |
| 925 | + output: null, |
| 926 | + errors: [{ messageId: 'mismatch' }], |
| 927 | + }, |
| 928 | + { |
| 929 | + code: '<tbody role="button"></tbody>', |
| 930 | + output: null, |
| 931 | + errors: [{ messageId: 'mismatch' }], |
| 932 | + }, |
| 933 | + |
836 | 934 | // DIVERGENCE: :recommended allows these; :strict and ours flag. |
837 | 935 | { |
838 | 936 | code: '<ul role="menu"></ul>', |
|
0 commit comments