|
| 1 | +// Audit fixture — peer-plugin parity for |
| 2 | +// `ember/template-no-unsupported-role-attributes`. |
| 3 | +// |
| 4 | +// Source files (context/ checkouts): |
| 5 | +// - eslint-plugin-jsx-a11y-main/src/rules/role-supports-aria-props.js |
| 6 | +// - eslint-plugin-jsx-a11y-main/__tests__/src/rules/role-supports-aria-props-test.js |
| 7 | +// - eslint-plugin-lit-a11y/lib/rules/role-supports-aria-attr.js |
| 8 | +// - eslint-plugin-lit-a11y/tests/lib/rules/role-supports-aria-attr.js |
| 9 | +// |
| 10 | +// These tests are NOT part of the main suite and do not run in CI. They encode |
| 11 | +// the CURRENT behavior of our rule. Each divergence from an upstream plugin is |
| 12 | +// annotated as "DIVERGENCE —". |
| 13 | + |
| 14 | +'use strict'; |
| 15 | + |
| 16 | +const rule = require('../../../lib/rules/template-no-unsupported-role-attributes'); |
| 17 | +const RuleTester = require('eslint').RuleTester; |
| 18 | + |
| 19 | +const ruleTester = new RuleTester({ |
| 20 | + parser: require.resolve('ember-eslint-parser'), |
| 21 | + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, |
| 22 | +}); |
| 23 | + |
| 24 | +ruleTester.run('audit:role-supports-aria-props (gts)', rule, { |
| 25 | + valid: [ |
| 26 | + // === Upstream parity (valid in jsx-a11y + ours) === |
| 27 | + '<template><div /></template>', |
| 28 | + '<template><div id="main" /></template>', |
| 29 | + |
| 30 | + // Explicit role with supported attr. |
| 31 | + '<template><div role="heading" aria-level="1" /></template>', |
| 32 | + '<template><div role="button" aria-disabled="true" /></template>', |
| 33 | + '<template><div role="textbox" aria-required="true" aria-errormessage="err" /></template>', |
| 34 | + '<template><span role="checkbox" aria-checked={{this.checked}} /></template>', |
| 35 | + |
| 36 | + // Implicit role tests that match between jsx-a11y and aria-query |
| 37 | + // (we rely on aria-query's elementRoles). |
| 38 | + // a with href — aria-query gives "generic" for first match; jsx-a11y |
| 39 | + // gives "link". Both happen to support aria-describedby etc. |
| 40 | + '<template><a href="#" aria-describedby="x"></a></template>', |
| 41 | + // input[type=submit] — implicit "button". |
| 42 | + '<template><input type="submit" aria-disabled="true" /></template>', |
| 43 | + // select — implicit "combobox". |
| 44 | + '<template><select aria-expanded="false" aria-controls="ctrlID" /></template>', |
| 45 | + // menu[type=toolbar] — aria-query gives "list"; jsx-a11y gives "toolbar". |
| 46 | + // aria-hidden is a global attr supported by both — valid in both. |
| 47 | + '<template><menu type="toolbar" aria-hidden="true" /></template>', |
| 48 | + |
| 49 | + // Components / unknown elements are skipped. |
| 50 | + '<template><CustomComponent role="banner" /></template>', |
| 51 | + '<template><some-custom-element /></template>', |
| 52 | + |
| 53 | + // Dynamic role (mustache value) — we skip. |
| 54 | + // jsx-a11y similarly skips non-literal role values. |
| 55 | + |
| 56 | + // === DIVERGENCE — <a> without href === |
| 57 | + // jsx-a11y: `<a>` without href has NO implicit role → uses global aria |
| 58 | + // set → `<a aria-checked />` is VALID. |
| 59 | + // Our rule: aria-query's first entry for `a` has no attribute constraint |
| 60 | + // and returns "generic". `generic` does not support aria-checked → we |
| 61 | + // would FLAG. (Invalid section captures this.) |
| 62 | + // Captured as the opposite: `<a aria-describedby="x">` passes in both |
| 63 | + // because aria-describedby is global. |
| 64 | + '<template><a aria-describedby="x"></a></template>', |
| 65 | + ], |
| 66 | + invalid: [ |
| 67 | + // === Upstream parity (invalid in jsx-a11y + ours) === |
| 68 | + // Explicit role rejects unsupported attrs. |
| 69 | + { |
| 70 | + code: '<template><div role="link" href="#" aria-checked /></template>', |
| 71 | + output: '<template><div role="link" href="#" /></template>', |
| 72 | + errors: [{ messageId: 'unsupportedExplicit' }], |
| 73 | + }, |
| 74 | + { |
| 75 | + code: '<template><div role="option" aria-notreal="x" aria-selected="false" /></template>', |
| 76 | + output: '<template><div role="option" aria-selected="false" /></template>', |
| 77 | + errors: [{ messageId: 'unsupportedExplicit' }], |
| 78 | + }, |
| 79 | + { |
| 80 | + code: '<template><div role="combobox" aria-multiline="true" aria-expanded="false" aria-controls="x" /></template>', |
| 81 | + output: '<template><div role="combobox" aria-expanded="false" aria-controls="x" /></template>', |
| 82 | + errors: [{ messageId: 'unsupportedExplicit' }], |
| 83 | + }, |
| 84 | + { |
| 85 | + code: '<template><a role="menuitem" aria-checked={{this.checked}} /></template>', |
| 86 | + output: '<template><a role="menuitem" /></template>', |
| 87 | + errors: [{ messageId: 'unsupportedExplicit' }], |
| 88 | + }, |
| 89 | + |
| 90 | + // Implicit role rejects unsupported attrs (parity). |
| 91 | + { |
| 92 | + code: '<template><button type="submit" aria-valuetext="x"></button></template>', |
| 93 | + output: '<template><button type="submit"></button></template>', |
| 94 | + errors: [{ messageId: 'unsupportedImplicit' }], |
| 95 | + }, |
| 96 | + { |
| 97 | + code: '<template><input type="button" aria-invalid="grammar" /></template>', |
| 98 | + output: '<template><input type="button" /></template>', |
| 99 | + errors: [{ messageId: 'unsupportedImplicit' }], |
| 100 | + }, |
| 101 | + |
| 102 | + // === DIVERGENCE — role-name in message differs for <menu type="toolbar"> === |
| 103 | + // jsx-a11y reports role "toolbar"; we report role "list". |
| 104 | + // Both FLAG though, so the divergence is cosmetic (message text). |
| 105 | + { |
| 106 | + code: '<template><menu type="toolbar" aria-expanded="true" /></template>', |
| 107 | + output: '<template><menu type="toolbar" /></template>', |
| 108 | + errors: [ |
| 109 | + { |
| 110 | + message: |
| 111 | + 'The attribute aria-expanded is not supported by the element menu with the implicit role of list', |
| 112 | + }, |
| 113 | + ], |
| 114 | + }, |
| 115 | + |
| 116 | + // === DIVERGENCE — role-name differs for <body> === |
| 117 | + // jsx-a11y: <body> implicit role = "document". |
| 118 | + // Our rule: aria-query first match gives role "generic". |
| 119 | + // aria-expanded is unsupported by both, so both FLAG — diff is message. |
| 120 | + { |
| 121 | + code: '<template><body aria-expanded="true"></body></template>', |
| 122 | + output: '<template><body></body></template>', |
| 123 | + errors: [ |
| 124 | + { |
| 125 | + message: |
| 126 | + 'The attribute aria-expanded is not supported by the element body with the implicit role of generic', |
| 127 | + }, |
| 128 | + ], |
| 129 | + }, |
| 130 | + |
| 131 | + // === Parity — <input type="email"> without `list` → textbox === |
| 132 | + // jsx-a11y considers these to be "textbox" (since aria-query's first |
| 133 | + // "email" entry has "list attribute not set" constraint → textbox). |
| 134 | + // Our rule now honors aria-query attribute constraints: `type=email` |
| 135 | + // without a `list` attribute maps to "textbox". With `list=...` it |
| 136 | + // maps to "combobox" (sibling case below). |
| 137 | + // aria-level is not supported by either role; still flagged. |
| 138 | + { |
| 139 | + code: '<template><input type="email" aria-level={{this.level}} /></template>', |
| 140 | + output: '<template><input type="email" /></template>', |
| 141 | + errors: [ |
| 142 | + { |
| 143 | + message: |
| 144 | + 'The attribute aria-level is not supported by the element input with the implicit role of textbox', |
| 145 | + }, |
| 146 | + ], |
| 147 | + }, |
| 148 | + // <input type="email" list="x"> → "combobox" (aria-level unsupported there too). |
| 149 | + { |
| 150 | + code: '<template><input type="email" list="x" aria-level={{this.level}} /></template>', |
| 151 | + output: '<template><input type="email" list="x" /></template>', |
| 152 | + errors: [ |
| 153 | + { |
| 154 | + message: |
| 155 | + 'The attribute aria-level is not supported by the element input with the implicit role of combobox', |
| 156 | + }, |
| 157 | + ], |
| 158 | + }, |
| 159 | + |
| 160 | + // === DIVERGENCE — <a> without href, with non-global aria attr === |
| 161 | + // jsx-a11y: VALID (no implicit role → global set). |
| 162 | + // Our rule: role=generic, aria-checked not supported → FLAG. FALSE POSITIVE. |
| 163 | + { |
| 164 | + code: '<template><a aria-checked /></template>', |
| 165 | + output: '<template><a /></template>', |
| 166 | + errors: [{ messageId: 'unsupportedImplicit' }], |
| 167 | + }, |
| 168 | + ], |
| 169 | +}); |
| 170 | + |
| 171 | +const hbsRuleTester = new RuleTester({ |
| 172 | + parser: require.resolve('ember-eslint-parser/hbs'), |
| 173 | + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, |
| 174 | +}); |
| 175 | + |
| 176 | +hbsRuleTester.run('audit:role-supports-aria-props (hbs)', rule, { |
| 177 | + valid: [ |
| 178 | + '<div />', |
| 179 | + '<div role="heading" aria-level="1" />', |
| 180 | + '<div role="button" aria-disabled="true" />', |
| 181 | + '<a href="#" aria-describedby=""></a>', |
| 182 | + '<menu type="toolbar" aria-hidden="true" />', |
| 183 | + '<input type="submit" aria-disabled="true" />', |
| 184 | + ], |
| 185 | + invalid: [ |
| 186 | + { |
| 187 | + code: '<div role="link" href="#" aria-checked />', |
| 188 | + output: '<div role="link" href="#" />', |
| 189 | + errors: [{ message: 'The attribute aria-checked is not supported by the role link' }], |
| 190 | + }, |
| 191 | + { |
| 192 | + code: '<menu type="toolbar" aria-expanded="true" />', |
| 193 | + output: '<menu type="toolbar" />', |
| 194 | + errors: [ |
| 195 | + { |
| 196 | + message: |
| 197 | + 'The attribute aria-expanded is not supported by the element menu with the implicit role of list', |
| 198 | + }, |
| 199 | + ], |
| 200 | + }, |
| 201 | + // DIVERGENCE: <a aria-checked /> — jsx-a11y says valid (no implicit role). |
| 202 | + // We flag with implicit role "generic". |
| 203 | + { |
| 204 | + code: '<a aria-checked />', |
| 205 | + output: '<a />', |
| 206 | + errors: [{ messageId: 'unsupportedImplicit' }], |
| 207 | + }, |
| 208 | + ], |
| 209 | +}); |
0 commit comments