Skip to content

Commit 05e668a

Browse files
committed
test: add Phase 3 audit fixture for no-noninteractive-tabindex
Translates jsx-a11y's no-noninteractive-tabindex test list to HBS to pin our rule's behavior against PR #24's interactive-element and tabindex="-1" exemption fixes. Parity holds on all upstream always-valid and never-valid cases. Annotated divergences: - tabpanel: we flag (jsx-a11y strict parity); jsx-a11y recommended whitelists it via `roles: ['tabpanel']`. - Dynamic role expressions: we skip conservatively; matches jsx-a11y recommended (allowExpressionValues: true), diverges from strict. - Component name collides with a native tag (e.g. <Article>): we lowercase tag before the aria-query dom lookup so <Article> validates as <article>. False positive relative to jsx-a11y's no-settings default.
1 parent 0b00272 commit 05e668a

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Audit fixture — translated test cases from jsx-a11y to measure behavioral
2+
// parity of `ember/template-no-noninteractive-tabindex` against
3+
// jsx-a11y/no-noninteractive-tabindex.
4+
//
5+
// These tests are NOT part of the main suite and do not run in CI. They encode
6+
// the CURRENT behavior of our rule so that running this file reports pass.
7+
// Each divergence from jsx-a11y is annotated as "DIVERGENCE —".
8+
//
9+
// Source file (context/ checkout):
10+
// - eslint-plugin-jsx-a11y-main/__tests__/src/rules/no-noninteractive-tabindex-test.js
11+
//
12+
// Translation notes:
13+
// - JSX `tabIndex="0"` → HBS `tabindex="0"`. Peer tests use camelCase
14+
// `tabIndex`; Ember is lowercase `tabindex`.
15+
// - JSX `{0}` / `{-1}` literal expressions → HBS `{{0}}` / `{{-1}}`.
16+
//
17+
// Rule-option notes:
18+
// - jsx-a11y ships two configs: `recommended` (with `roles: ['tabpanel']`
19+
// and `allowExpressionValues: true`) and `strict` (no options). Our rule
20+
// takes no options. We map jsx-a11y's RECOMMENDED config for
21+
// `allowExpressionValues`-style behavior (dynamic role expressions pass),
22+
// and jsx-a11y's STRICT config for `tabpanel` (it's flagged). Where these
23+
// collide we note the specific divergence.
24+
25+
'use strict';
26+
27+
const rule = require('../../../lib/rules/template-no-noninteractive-tabindex');
28+
const RuleTester = require('eslint').RuleTester;
29+
30+
const ruleTester = new RuleTester({
31+
parser: require.resolve('ember-eslint-parser'),
32+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
33+
});
34+
35+
ruleTester.run('audit:no-noninteractive-tabindex (gts)', rule, {
36+
valid: [
37+
// === Upstream parity (alwaysValid — valid in jsx-a11y and ours) ===
38+
39+
// Component with tabindex — jsx-a11y: skipped (unknown component).
40+
// Ours: skipped (not in aria-query dom map).
41+
'<template><MyButton tabindex={{0}} /></template>',
42+
43+
// No tabindex — rule doesn't fire.
44+
'<template><button /></template>',
45+
46+
// Interactive native element with tabindex — valid.
47+
'<template><button tabindex="0" /></template>',
48+
'<template><button tabindex={{0}} /></template>',
49+
50+
// No tabindex on non-interactive element — valid.
51+
'<template><div /></template>',
52+
53+
// tabindex="-1" exemption — focusable but out of tab order. Valid in both.
54+
// (jsx-a11y: `getTabIndex` returns -1, rule short-circuits. Ours:
55+
// `getStaticTabindexValue === -1` short-circuit added in PR #24.)
56+
'<template><div tabindex="-1" /></template>',
57+
58+
// Non-interactive element made interactive via role — valid.
59+
'<template><div role="button" tabindex="0"></div></template>',
60+
61+
// Non-interactive role + tabindex="-1" — valid via -1 exemption.
62+
'<template><div role="article" tabindex="-1"></div></template>',
63+
64+
// Non-interactive native element + tabindex="-1" — valid via -1 exemption.
65+
'<template><article tabindex="-1"></article></template>',
66+
67+
// === DIVERGENCE — components whose name lowercases to a native tag ===
68+
// jsx-a11y `components` setting maps `<Article>` → `article`, then treats
69+
// it like the native tag. Without such a setting jsx-a11y would treat
70+
// `<Article>` as an opaque component and skip.
71+
// Our rule lowercases `node.tag` before the `dom.has(tag)` check, so
72+
// `<Article>` becomes `article` and IS validated against the dom map.
73+
// This has two effects:
74+
// (a) `<Article tabindex="-1" />` passes via the tabindex=-1 exemption
75+
// (valid in jsx-a11y too, so no visible divergence here).
76+
// (b) `<Article tabindex={{0}} />` is FLAGGED by our rule (see invalid
77+
// section below). jsx-a11y without `components` setting: VALID (opaque
78+
// component skip). jsx-a11y with `components: {Article: 'article'}`
79+
// setting: INVALID. Our rule: INVALID regardless — false positive
80+
// for the no-settings case.
81+
// Components whose name does NOT collide with a native tag (e.g.
82+
// `<MyButton>` → `mybutton` which is not in the dom map) are correctly
83+
// skipped.
84+
'<template><Article tabindex="-1" /></template>',
85+
'<template><MyButton tabindex={{0}} /></template>',
86+
87+
// === Upstream parity (jsx-a11y recommended valid) ===
88+
89+
// jsx-a11y recommended: `tabpanel` whitelisted via `roles` option → valid.
90+
// jsx-a11y strict: `tabpanel` flagged (it's not an interactive role).
91+
// Our rule has no options and treats `tabpanel` as non-interactive per
92+
// aria-query's role graph. See the invalid section for the strict-side
93+
// assertion.
94+
95+
// jsx-a11y recommended: dynamic role expressions pass because
96+
// `allowExpressionValues: true`. Our rule conservatively skips dynamic
97+
// roles too. Parity with recommended config.
98+
'<template><div role={{this.role}} tabindex="0"></div></template>',
99+
100+
// === DIVERGENCE — tabpanel tabindex classification ===
101+
// jsx-a11y recommended: `<div role="tabpanel" tabindex="0" />` VALID
102+
// (tabpanel is in the recommended `roles` whitelist).
103+
// jsx-a11y strict: INVALID.
104+
// Our rule: INVALID (tabpanel is not in INTERACTIVE_ROLES). We match
105+
// jsx-a11y strict, diverge from recommended. Captured in invalid section.
106+
],
107+
108+
invalid: [
109+
// === Upstream parity (neverValid — invalid in jsx-a11y and ours) ===
110+
111+
// Plain non-interactive element with tabindex="0" — invalid.
112+
{
113+
code: '<template><div tabindex="0"></div></template>',
114+
output: null,
115+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
116+
},
117+
118+
// Non-interactive role doesn't rescue tabindex="0" — invalid.
119+
{
120+
code: '<template><div role="article" tabindex="0"></div></template>',
121+
output: null,
122+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
123+
},
124+
125+
// Non-interactive native element with tabindex="0" — invalid.
126+
{
127+
code: '<template><article tabindex="0"></article></template>',
128+
output: null,
129+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
130+
},
131+
132+
// Mustache-number form — parity invalid.
133+
{
134+
code: '<template><article tabindex={{0}}></article></template>',
135+
output: null,
136+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
137+
},
138+
139+
// === DIVERGENCE — tabpanel in strict mode ===
140+
// jsx-a11y strict: INVALID (tabpanel not interactive).
141+
// jsx-a11y recommended: VALID (`roles: ['tabpanel']` whitelist).
142+
// Our rule: INVALID. Matches jsx-a11y strict, diverges from recommended.
143+
{
144+
code: '<template><div role="tabpanel" tabindex="0"></div></template>',
145+
output: null,
146+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
147+
},
148+
149+
// === DIVERGENCE — jsx-a11y strict: expressions as role values ===
150+
// jsx-a11y strict: `<div role={ROLE_BUTTON} ... tabIndex="0" />` INVALID
151+
// because `allowExpressionValues` defaults to false and `isNonLiteralProperty`
152+
// flags the expression role as unknown.
153+
// jsx-a11y recommended: VALID (allowExpressionValues: true).
154+
// Our rule: VALID — we conservatively skip dynamic roles. No invalid
155+
// assertion here; captured as a comment only. Our behavior matches
156+
// jsx-a11y RECOMMENDED, not strict.
157+
158+
// === DIVERGENCE — component name collides with a native tag ===
159+
// `<Article tabIndex={0} />` — classifications:
160+
// jsx-a11y without `components` setting: VALID (opaque component skip).
161+
// jsx-a11y with `components: {Article: 'article'}`: INVALID (article
162+
// is non-interactive).
163+
// Our rule: INVALID unconditionally. We lowercase `node.tag` before the
164+
// `dom.has(tag)` check, so `Article` → `article` is found in the dom
165+
// map and validated like the native tag. This is a FALSE POSITIVE
166+
// relative to jsx-a11y's no-settings default, and accidental parity
167+
// with jsx-a11y's components-configured mode.
168+
// Components whose lowercased name doesn't collide with a native tag
169+
// (e.g. `<MyButton>`) are correctly skipped.
170+
{
171+
code: '<template><Article tabindex={{0}} /></template>',
172+
output: null,
173+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
174+
},
175+
],
176+
});
177+
178+
const hbsRuleTester = new RuleTester({
179+
parser: require.resolve('ember-eslint-parser/hbs'),
180+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
181+
});
182+
183+
hbsRuleTester.run('audit:no-noninteractive-tabindex (hbs)', rule, {
184+
valid: [
185+
// Parity — interactive native element / role / tabindex="-1" exemption.
186+
'<button tabindex="0"></button>',
187+
'<div />',
188+
'<div tabindex="-1"></div>',
189+
'<div role="button" tabindex="0"></div>',
190+
'<article tabindex="-1"></article>',
191+
// Dynamic role — parity with jsx-a11y recommended (allowExpressionValues: true).
192+
'<div role={{this.role}} tabindex="0"></div>',
193+
// Non-tag-colliding component — skipped (not in aria-query dom map).
194+
// Parity with jsx-a11y's no-settings default.
195+
// (Components whose lowercased name collides with a native tag diverge;
196+
// see `<Article tabindex={{0}} />` in the gts invalid section.)
197+
'<MyButton tabindex="0" />',
198+
],
199+
invalid: [
200+
// Parity — neverValid cases in hbs form.
201+
{
202+
code: '<div tabindex="0"></div>',
203+
output: null,
204+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
205+
},
206+
{
207+
code: '<div role="article" tabindex="0"></div>',
208+
output: null,
209+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
210+
},
211+
{
212+
code: '<article tabindex="0"></article>',
213+
output: null,
214+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
215+
},
216+
// DIVERGENCE — tabpanel strict-mode classification (see gts section).
217+
{
218+
code: '<div role="tabpanel" tabindex="0"></div>',
219+
output: null,
220+
errors: [{ messageId: 'noNonInteractiveTabindex' }],
221+
},
222+
],
223+
});

0 commit comments

Comments
 (0)