Skip to content

Commit 2e20fcd

Browse files
committed
test: add Phase 3 audit fixture for no-role-presentation-on-focusable
1 parent c03ea36 commit 2e20fcd

1 file changed

Lines changed: 200 additions & 0 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Audit fixture — translated test cases from peer plugins to measure
2+
// behavioral parity of `ember/template-no-role-presentation-on-focusable`
3+
// against vuejs-accessibility/no-role-presentation-on-focusable.
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 an upstream plugin is annotated as "DIVERGENCE —".
8+
//
9+
// Only vuejs-accessibility carries this rule among surveyed peer a11y plugins.
10+
//
11+
// Source file:
12+
// - context/eslint-plugin-vuejs-accessibility-main/src/rules/__tests__/no-role-presentation-on-focusable.test.ts
13+
14+
'use strict';
15+
16+
const rule = require('../../../lib/rules/template-no-role-presentation-on-focusable');
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:no-role-presentation-on-focusable (gts)', rule, {
25+
valid: [
26+
// === Upstream parity (valid in both vue-a11y and us) ===
27+
// No role at all on focusable element — both skip.
28+
'<template><button>Submit</button></template>',
29+
// Nested focusable without role — both skip.
30+
'<template><div><button>Submit</button></div></template>',
31+
// `a` with tabindex='-1' but no role — both skip.
32+
'<template><a href="#" tabindex="-1">link</a></template>',
33+
34+
// Non-focusable container with role="presentation" wrapping a focusable
35+
// child that is itself defocused via tabindex='-1'. Vue: valid (its
36+
// hasFocusableElements recurses, but finds no focusable descendant because
37+
// the child's tabindex='-1' short-circuits). Ours: valid for the outer div
38+
// (div isn't focusable itself) AND for the inner button (no role on it).
39+
// Happens to match vue — but note the mechanisms differ:
40+
// - Vue's rule checks element + descendants.
41+
// - Our rule checks only the element bearing the role.
42+
'<template><div role="presentation"><button tabindex="-1">Some text</button></div></template>',
43+
44+
// Same pattern with an `a` child that is defocused via tabindex='-1'.
45+
'<template><div role="presentation"><a href="#" tabindex="-1">Link</a></div></template>',
46+
],
47+
48+
invalid: [
49+
// === Upstream parity (invalid in both vue-a11y and us) ===
50+
// <button> is inherently focusable — both flag.
51+
{
52+
code: '<template><button type="button" role="presentation">Submit</button></template>',
53+
output: null,
54+
errors: [{ messageId: 'invalidPresentation' }],
55+
},
56+
// <a href> is inherently focusable — both flag.
57+
{
58+
code: '<template><a href="#" role="presentation">Link</a></template>',
59+
output: null,
60+
errors: [{ messageId: 'invalidPresentation' }],
61+
},
62+
// <span tabindex="0"> is focusable via tabindex — both flag.
63+
{
64+
code: '<template><span tabindex="0" role="presentation"><em>Icon</em></span></template>',
65+
output: null,
66+
errors: [{ messageId: 'invalidPresentation' }],
67+
},
68+
69+
// === INTENTIONAL DIVERGENCE — tabindex="-1" on inherently-focusable ===
70+
// Vue: VALID — `<button tabindex='-1' role='presentation'>Press</button>`.
71+
// Vue's hasFocusableElements treats an interactive element with
72+
// tabindex='-1' as non-focusable.
73+
// Our rule: INVALID — a `<button>` with `tabindex='-1'` is still
74+
// programmatically focusable (focus() / sequential-focus-exclusion only
75+
// removes it from tab order). The element can still receive focus, at
76+
// which point it's announced by AT. Flagging is intentional.
77+
// Same tabindex stance taken by `template-no-aria-hidden-on-focusable`
78+
// (#19) — see PR #22 body for rationale.
79+
{
80+
code: '<template><button tabindex="-1" role="presentation">Press</button></template>',
81+
output: null,
82+
errors: [{ messageId: 'invalidPresentation' }],
83+
},
84+
],
85+
});
86+
87+
// === INTENTIONAL DIVERGENCE — scope: element-only vs. element + descendants ===
88+
// Vue's rule flags a non-focusable container with role="presentation" when any
89+
// DESCENDANT is focusable:
90+
// <div role='presentation'><button>Submit</button></div> → vue: INVALID
91+
// Our rule only inspects the element bearing the role. A non-focusable wrapper
92+
// is left alone; the authoring error vue targets (stripping semantics from
93+
// a container whose children remain focusable) is not detected by us.
94+
//
95+
// This is a known gap, not a bug fix waiting to happen:
96+
// - The WAI-ARIA §4.6 conflict-resolution text is phrased for elements with
97+
// role=presentation that are themselves focusable. Applying it to ancestors
98+
// is vue's interpretation, not a spec mandate.
99+
// - Our rule is narrower and has fewer false positives, at the cost of this
100+
// true positive.
101+
// Captured here as a "valid" bucket for OUR rule to make the divergence
102+
// explicit in the audit output.
103+
ruleTester.run('audit:no-role-presentation-on-focusable — wrapper scope (gts)', rule, {
104+
valid: [
105+
// Vue: INVALID (outer div flagged because descendant <button> is focusable).
106+
// Ours: VALID (div isn't itself focusable; we don't descend).
107+
'<template><div role="presentation"><button>Submit</button></div></template>',
108+
// Same shape with <a href>.
109+
'<template><div role="presentation"><a href="#">Link</a></div></template>',
110+
// Same shape with tabindex on a descendant.
111+
'<template><div role="presentation"><span tabindex="0">Focusable</span></div></template>',
112+
],
113+
invalid: [],
114+
});
115+
116+
// === INTENTIONAL EXTENSION — role="none" treated identically to "presentation" ===
117+
// Vue's test suite exercises only role="presentation". Per WAI-ARIA 1.2,
118+
// role="none" is a synonym introduced to avoid the misleading "presentation"
119+
// name. Our rule flags both. No conflict with vue — vue simply doesn't test
120+
// "none" — but captured here so the extension is visible in the audit.
121+
ruleTester.run('audit:no-role-presentation-on-focusable — role="none" extension (gts)', rule, {
122+
valid: [
123+
// Non-focusable element with role="none" — fine.
124+
'<template><div role="none"></div></template>',
125+
],
126+
invalid: [
127+
// Inherently focusable element with role="none" — we flag.
128+
{
129+
code: '<template><button role="none">Click</button></template>',
130+
output: null,
131+
errors: [{ messageId: 'invalidPresentation' }],
132+
},
133+
// <a href> with role="none" — we flag.
134+
{
135+
code: '<template><a href="/x" role="none">Link</a></template>',
136+
output: null,
137+
errors: [{ messageId: 'invalidPresentation' }],
138+
},
139+
],
140+
});
141+
142+
const hbsRuleTester = new RuleTester({
143+
parser: require.resolve('ember-eslint-parser/hbs'),
144+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
145+
});
146+
147+
hbsRuleTester.run('audit:no-role-presentation-on-focusable (hbs)', rule, {
148+
valid: [
149+
// Upstream parity — no role.
150+
'<button>Submit</button>',
151+
'<div><button>Submit</button></div>',
152+
'<a href="#" tabindex="-1">link</a>',
153+
// Upstream parity — wrapper whose descendants are all defocused.
154+
'<div role="presentation"><button tabindex="-1">Some text</button></div>',
155+
'<div role="presentation"><a href="#" tabindex="-1">Link</a></div>',
156+
157+
// DIVERGENCE (wrapper scope) — captured as valid for us; vue flags.
158+
'<div role="presentation"><button>Submit</button></div>',
159+
'<div role="presentation"><a href="#">Link</a></div>',
160+
161+
// EXTENSION — role="none" on non-focusable element is fine for us.
162+
'<div role="none"></div>',
163+
],
164+
invalid: [
165+
// Upstream parity — both flag.
166+
{
167+
code: '<button type="button" role="presentation">Submit</button>',
168+
output: null,
169+
errors: [{ messageId: 'invalidPresentation' }],
170+
},
171+
{
172+
code: '<a href="#" role="presentation">Link</a>',
173+
output: null,
174+
errors: [{ messageId: 'invalidPresentation' }],
175+
},
176+
{
177+
code: '<span tabindex="0" role="presentation"><em>Icon</em></span>',
178+
output: null,
179+
errors: [{ messageId: 'invalidPresentation' }],
180+
},
181+
182+
// INTENTIONAL DIVERGENCE — tabindex="-1" on button. Vue: valid. Ours: flag.
183+
{
184+
code: '<button tabindex="-1" role="presentation">Press</button>',
185+
output: null,
186+
errors: [{ messageId: 'invalidPresentation' }],
187+
},
188+
189+
// INTENTIONAL EXTENSION — role="none" treated as presentation.
190+
{
191+
code: '<button role="none">Click</button>',
192+
output: null,
193+
errors: [{ messageId: 'invalidPresentation' }],
194+
},
195+
],
196+
});
197+
198+
// AUDIT-SKIP — none.
199+
// All vue-a11y test cases from no-role-presentation-on-focusable.test.ts are
200+
// represented above (6 valid + 4 invalid in the source file).

0 commit comments

Comments
 (0)