Skip to content

BUGFIX: template-require-valid-alt-text — reject empty-string aria-label/labelledby/alt on <input type=image>, <object>, <area>#56

Draft
johanrd wants to merge 10 commits intomasterfrom
fix/alt-text-empty-aria-label
Draft

BUGFIX: template-require-valid-alt-text — reject empty-string aria-label/labelledby/alt on <input type=image>, <object>, <area>#56
johanrd wants to merge 10 commits intomasterfrom
fix/alt-text-empty-aria-label

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 22, 2026

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

[Mirror of ember-cli#2730 for Copilot review]

Summary

  • Premise 1: An empty-string aria-label="" contributes no accessible name per ACCNAME 1.2 §4.3.2 step 2D: "…has an aria-label attribute whose value is not undefined, not the empty string, nor, when trimmed of whitespace, is not the empty string." Empty aria-labelledby="" likewise contributes no name per ACCNAME 1.2 §4.3.2 step 2B, which requires the attribute to "contain at least one valid IDREF" — an empty value references none. Empty title="" provides no useful accessible name. ACCNAME 1.2 step 2I (Tooltip) literally says "if the current node has a Tooltip attribute, return its value" — there is no emptiness check at this step, so an empty title returns the empty string as a candidate name. That empty name does not satisfy the host-language conformance requirements that follow. For <input type="image">, <object>, and <area>, an accessible name is required per their HTML spec / HTML-AAM conformance requirements.
  • Premise 2: Today our rule checks only for the presence of any fallback attribute, not its value — so <input type="image" aria-label="" />, <object aria-labelledby="" />, and <area title="" /> are accepted.
  • Conclusion: Reject empty / whitespace-only aria-label, aria-labelledby, and title on <input type="image">, <object>, and <area>. Dynamic values (mustache, concat) stay accepted — we can't know at lint time whether they resolve to empty.

Fix: add hasNonEmptyTextAttr() that requires a static value to be non-whitespace.

<img>'s alt="" is unchanged — an empty alt on <img> is spec-defined as a marker for decorative images.

Nine new invalid tests (3 elements × 3 fallback attrs) cover the fix.

Prior art

Verified each peer in source:

Plugin Rule Behavior
jsx-a11y alt-text ariaLabelHasValue (lines 37-46) rejects undefined and length === 0. Called for object, area, and input[type="image"]. Does NOT trim whitespace (differs from our rule which does).
vuejs-accessibility alt-text hasAriaLabel(node) + getElementAttributeValue truthiness. Empty strings are falsy → flagged.
@angular-eslint/template alt-text Existence-onlyisValidObjectNode/isValidAreaNode/isValidInputNode all return true on mere attribute-name presence (same bug class this PR fixes).
lit-a11y alt-text Existence-only via !elementHasAttribute(...). Also: no <object> or <area> handler — rule only checks img, input[type=image], role="img".

Audit fixture

Translated peer-plugin test fixture at tests/audit/alt-text/peer-parity.js. Runs as part of the default Vitest suite (included via the tests/**/*.js glob) and serves double-duty as an auditable peer-parity record and as regression coverage pinning current behavior.

johanrd added 3 commits April 21, 2026 07:50
…aria-labelledby/alt

Before: for <input type="image">, <object>, and <area>, the rule checked
only for the PRESENCE of an accessible-name fallback attribute
(aria-label / aria-labelledby / alt / title). An empty-string value
provides no accessible name but slipped past.

Fix: add hasNonEmptyTextAttr() that requires the attribute's static
value to be non-whitespace. Dynamic values (mustache, concat) remain
accepted — we can't tell at lint time whether they resolve to empty.

<img>'s alt handling is unchanged — alt="" is still valid there
(spec-defined marker for decorative images).

Nine new invalid tests cover the three elements × three fallback attrs.
Translates 41 cases from peer-plugin rules:
  - jsx-a11y alt-text
  - vuejs-accessibility alt-text
  - lit-a11y alt-text

Fixture documents parity after this fix:
  - Empty-string aria-label/aria-labelledby on <object>, <area>, and
    <input type=image> is now flagged (reusing existing objectMissing /
    areaMissing / inputImage messageIds).

Remaining divergences (<img alt role=presentation> accepting non-empty
alt in jsx-a11y, <img aria-label> without alt) are annotated inline.
@johanrd johanrd requested a review from Copilot April 22, 2026 10:41
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
js small 14.56 ms 14.41 ms -1.0%
js medium 7.18 ms 7.10 ms -1.2%
🟢 js large 2.94 ms 2.77 ms -5.6%
gjs small 1.24 ms 1.23 ms -0.8%
gjs medium 612.28 µs 610.22 µs -0.3%
gjs large 244.46 µs 243.44 µs -0.4%
gts small 1.22 ms 1.22 ms -0.1%
gts medium 610.83 µs 611.65 µs +0.1%
gts large 243.54 µs 245.58 µs +0.8%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~2.44 GHz
cpu: AMD EPYC 7763 64-Core Processor
runtime: node 24.14.1 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            17.16 ms/iter  17.81 ms █                    
                      (12.49 ms … 28.84 ms)  28.43 ms █  ▃                 
                    (  5.29 mb …  10.62 mb)   7.26 mb █████▆▄▆▁▄▁▁▁▁▄▄▄▁▆▄▄

js small (experiment)         14.90 ms/iter  15.64 ms    █                 
                      (13.03 ms … 20.33 ms)  19.30 ms  ▃███ ▃ ▆            
                    (  6.44 mb …   8.21 mb)   6.83 mb ███████▁████▄▁▁▁▁▄▁▁▄

                             ┌                                            ┐
                             ╷┌───────────┬─┐                             ╷
          js small (control) ├┤           │ ├─────────────────────────────┤
                             ╵└───────────┴─┘                             ╵
                               ╷ ┌──┬─┐         ╷
       js small (experiment)   ├─┤  │ ├─────────┤
                               ╵ └──┴─┘         ╵
                             └                                            ┘
                             12.49 ms           20.46 ms           28.43 ms

summary
  js small (experiment)
   1.15x faster than js small (control)

------------------------------------------- -------------------------------
js medium (control)            7.89 ms/iter   8.44 ms  █                   
                       (6.78 ms … 13.37 ms)  13.18 ms ▇█                   
                    (  2.68 mb …   4.60 mb)   3.54 mb ███▃▂▆▅▃▁▂▃▂▁▂▁▂▁▁▂▁▃

js medium (experiment)         7.62 ms/iter   7.74 ms ▂█                   
                       (6.60 ms … 13.61 ms)  13.39 ms ██▃▂                 
                    (  2.99 mb …   4.02 mb)   3.53 mb ████▃▂█▃▃▂▁▂▂▁▁▁▁▁▁▁▃

                             ┌                                            ┐
                              ╷┌──────┬──┐                               ╷
         js medium (control)  ├┤      │  ├───────────────────────────────┤
                              ╵└──────┴──┘                               ╵
                             ╷┌─────┬┐                                    ╷
      js medium (experiment) ├┤     │├────────────────────────────────────┤
                             ╵└─────┴┘                                    ╵
                             └                                            ┘
                             6.60 ms            9.99 ms            13.39 ms

summary
  js medium (experiment)
   1.04x faster than js medium (control)

------------------------------------------- -------------------------------
js large (control)             3.82 ms/iter   4.32 ms █                    
                       (2.75 ms … 13.42 ms)  13.10 ms █                    
                    ( 81.55 kb …   2.98 mb)   1.44 mb █▄▃▆▃▂▂▂▁▂▁▁▁▁▁▁▁▁▁▁▁

js large (experiment)          3.06 ms/iter   2.94 ms  █                   
                        (2.58 ms … 7.60 ms)   6.90 ms  █                   
                    (654.94 kb …   2.62 mb)   1.43 mb ██▃▃▃▁▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                              ┌───┬─┐                                     ╷
          js large (control)  │   │ ├─────────────────────────────────────┤
                              └───┴─┘                                     ╵
                             ╷┌┬               ╷
       js large (experiment) ├┤│───────────────┤
                             ╵└┴               ╵
                             └                                            ┘
                             2.58 ms            7.84 ms            13.10 ms

summary
  js large (experiment)
   1.25x faster than js large (control)

------------------------------------------- -------------------------------
gjs small (control)            1.39 ms/iter   1.34 ms █                    
                        (1.20 ms … 6.58 ms)   5.35 ms █▂                   
                    (282.97 kb …   1.66 mb)   1.06 mb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.36 ms/iter   1.26 ms █                    
                        (1.19 ms … 6.36 ms)   5.47 ms █                    
                    (199.99 kb …   1.78 mb)   1.06 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                         ╷
         gjs small (control) │ │─────────────────────────────────────────┤
                             └─┴                                         ╵
                             ┌─┬                                          ╷
      gjs small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.19 ms            3.33 ms             5.47 ms

summary
  gjs small (experiment)
   1.03x faster than gjs small (control)

------------------------------------------- -------------------------------
gjs medium (control)         669.50 µs/iter 632.50 µs █                    
                      (581.29 µs … 5.83 ms)   3.42 ms █                    
                    ( 74.95 kb …   1.33 mb) 541.66 kb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      659.87 µs/iter 624.04 µs █                    
                      (581.52 µs … 5.49 ms)   2.16 ms █▂                   
                    ( 99.10 kb …   0.99 mb) 541.19 kb ██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
        gjs medium (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                       ╷
     gjs medium (experiment) ││───────────────────────┤
                             └┴                       ╵
                             └                                            ┘
                             581.29 µs           2.00 ms            3.42 ms

summary
  gjs medium (experiment)
   1.01x faster than gjs medium (control)

------------------------------------------- -------------------------------
gjs large (control)          268.88 µs/iter 258.38 µs  █                   
                      (231.82 µs … 4.96 ms) 397.37 µs ▇██▂                 
                    (216.10 kb …   1.03 mb) 217.64 kb █████▅▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁

gjs large (experiment)       265.05 µs/iter 257.99 µs  █                   
                      (232.73 µs … 5.15 ms) 318.43 µs  ███                 
                    ( 80.11 kb … 660.75 kb) 216.63 kb ▅███▄▃▇▆▆▄▂▂▂▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌────────┬                                  ╷
         gjs large (control) ├┤        │──────────────────────────────────┤
                             ╵└────────┴                                  ╵
                             ╷ ┌──────┬              ╷
      gjs large (experiment) ├─┤      │──────────────┤
                             ╵ └──────┴              ╵
                             └                                            ┘
                             231.82 µs         314.60 µs          397.37 µs

summary
  gjs large (experiment)
   1.01x faster than gjs large (control)

------------------------------------------- -------------------------------
gts small (control)            1.32 ms/iter   1.24 ms █                    
                        (1.18 ms … 6.29 ms)   4.92 ms █                    
                    (165.95 kb …   1.55 mb)   1.06 mb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.30 ms/iter   1.24 ms █                    
                        (1.18 ms … 6.20 ms)   5.08 ms █                    
                    (670.08 kb …   1.48 mb)   1.05 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                        ╷
         gts small (control) │ │────────────────────────────────────────┤
                             └─┴                                        ╵
                             ┌┬                                           ╷
      gts small (experiment) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             └                                            ┘
                             1.18 ms            3.13 ms             5.08 ms

summary
  gts small (experiment)
   1.01x faster than gts small (control)

------------------------------------------- -------------------------------
gts medium (control)         656.33 µs/iter 625.16 µs  █                   
                      (581.50 µs … 5.50 ms)   1.35 ms ▂█                   
                    (124.35 kb …   1.29 mb) 542.05 kb ██▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      656.59 µs/iter 626.34 µs  █                   
                      (582.13 µs … 5.30 ms)   1.62 ms ▇█                   
                    (292.38 kb …   1.25 mb) 541.05 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌─┬                             ╷
        gts medium (control) ├┤ │─────────────────────────────┤
                             ╵└─┴                             ╵
                             ╷┌─┬                                         ╷
     gts medium (experiment) ├┤ │─────────────────────────────────────────┤
                             ╵└─┴                                         ╵
                             └                                            ┘
                             581.50 µs           1.10 ms            1.62 ms

summary
  gts medium (control)
   1x faster than gts medium (experiment)

------------------------------------------- -------------------------------
gts large (control)          265.17 µs/iter 258.43 µs  █                   
                      (232.94 µs … 5.35 ms) 325.19 µs  █▆▂                 
                    (182.73 kb … 802.38 kb) 216.87 kb ▄███▃▄█▅▅▂▂▁▁▁▁▁▁▁▁▁▁

gts large (experiment)       267.67 µs/iter 261.55 µs  █▂                  
                      (235.39 µs … 5.50 ms) 335.21 µs  ██                  
                    (170.17 kb … 724.80 kb) 216.60 kb ▅██▇▃▆▆▆▃▂▁▂▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷ ┌───────────┬                          ╷
         gts large (control) ├─┤           │──────────────────────────┤
                             ╵ └───────────┴                          ╵
                              ╷ ┌───────────┬                             ╷
      gts large (experiment)  ├─┤           │─────────────────────────────┤
                              ╵ └───────────┴                             ╵
                             └                                            ┘
                             232.94 µs         284.07 µs          335.21 µs

summary
  gts large (control)
   1.01x faster than gts large (experiment)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens template-require-valid-alt-text so that empty/whitespace-only static accessible-name fallback attributes no longer satisfy the rule for elements that require an accessible name (<input type="image">, <object>, <area>), and adds regression tests (plus an audit parity fixture).

Changes:

  • Add hasNonEmptyTextAttr() / hasAnyNonEmptyTextAttr() to treat empty/whitespace-only static values as missing while still accepting dynamic values.
  • Update rule logic for <input type="image">, <object>, and <area> to use the non-empty attribute checks.
  • Add invalid test cases for empty-string fallbacks and introduce a peer-parity audit fixture.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
lib/rules/template-require-valid-alt-text.js Adds non-empty fallback attribute detection and applies it to input[type=image], object, and area checks.
tests/lib/rules/template-require-valid-alt-text.js Adds invalid tests asserting empty-string fallbacks are rejected for affected elements.
tests/audit/alt-text/peer-parity.js Adds an audit fixture intended to track parity/divergence with peer plugins’ alt-text rules.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-valid-alt-text.js Outdated
Comment thread tests/lib/rules/template-require-valid-alt-text.js
Comment thread tests/audit/alt-text/peer-parity.js Outdated
johanrd added 2 commits April 22, 2026 14:21
…verage (Copilot review)

- JSDoc for hasNonEmptyTextAttr() rewritten: no longer overstates the
  guarantee for dynamic values, and notes that aria-labelledby IDREFs
  are not validated.
- Added invalid-case coverage for whitespace-only aria-label /
  aria-labelledby / title — ACCNAME 1.2 §4.3.2 step 2D.
- hasNonEmptyTextAttr() already trims static values, so the new
  whitespace-only cases flag without further rule changes.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the template-require-valid-alt-text accessibility rule to treat empty/whitespace-only static fallback attributes as missing accessible names for elements that require one, aligning behavior more closely with ACCNAME expectations.

Changes:

  • Add hasNonEmptyTextAttr() / hasAnyNonEmptyTextAttr() and use them for <input type="image">, <object>, and <area> fallback checks.
  • Add new invalid test cases covering empty/whitespace-only fallbacks.
  • Add an “audit fixture” test file translating peer-plugin fixtures into assertions against this rule.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
lib/rules/template-require-valid-alt-text.js Implements non-empty static attribute checks and applies them to relevant elements.
tests/lib/rules/template-require-valid-alt-text.js Adds new invalid cases for empty/whitespace-only fallback attributes.
tests/audit/alt-text/peer-parity.js Adds an audit/regression fixture that runs RuleTester against peer-parity cases.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-valid-alt-text.js Outdated
Comment thread tests/lib/rules/template-require-valid-alt-text.js
Comment thread tests/lib/rules/template-require-valid-alt-text.js Outdated
Comment thread tests/audit/alt-text/peer-parity.js Outdated
@johanrd johanrd force-pushed the fix/alt-text-empty-aria-label branch from c4885d4 to 82022bb Compare April 22, 2026 17:09
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR tightens template-require-valid-alt-text so that “accessible-name-required” elements (<input type="image">, <object>, <area>) no longer treat empty/whitespace-only static fallback attributes (e.g. aria-label="") as satisfying the rule.

Changes:

  • Add hasNonEmptyTextAttr() / hasAnyNonEmptyTextAttr() and use them for <input type="image">, <object>, and <area> fallback checks.
  • Expand rule test coverage (GTS + HBS) to include empty-string and whitespace-only cases.
  • Add an audit fixture translating peer plugin tests into RuleTester assertions for parity/regression tracking.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/rules/template-require-valid-alt-text.js Enforces non-empty static fallback attributes for accessible-name-required elements.
tests/lib/rules/template-require-valid-alt-text.js Adds invalid cases for empty/whitespace-only fallback attributes (GTS + HBS parity).
tests/audit/alt-text/peer-parity.js Adds a peer-parity audit fixture executed by the Vitest glob.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-valid-alt-text.js
Comment thread tests/audit/alt-text/peer-parity.js Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an accessibility false-negative in ember/template-require-valid-alt-text by ensuring “accessible-name-required” elements are not considered valid when their fallback naming attributes are present but empty/whitespace.

Changes:

  • Add a helper to treat static empty/whitespace alt / aria-label / aria-labelledby / title as “missing” (while still accepting dynamic values).
  • Expand rule tests to cover empty-string and whitespace-only fallbacks in both GJS/GTS and HBS modes.
  • Add a Vitest “peer parity” audit fixture capturing current behavior and divergences vs peer plugins.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/rules/template-require-valid-alt-text.js Enforces non-empty static accessible-name fallbacks for <input type="image">, <object>, and <area>.
tests/lib/rules/template-require-valid-alt-text.js Adds invalid test cases for empty/whitespace fallbacks (GJS/GTS + HBS parity).
tests/audit/alt-text/peer-parity.js Introduces an audit/regression fixture translating peer-plugin cases into assertions against this rule.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-require-valid-alt-text.js
Comment thread tests/audit/alt-text/peer-parity.js Outdated
johanrd added 2 commits April 24, 2026 19:05
…t-a11y-behavior.md (Copilot review)

That doc was never checked in. Remove the dangling reference and note
that divergences are captured inline (grep for 'DIVERGENCE —') plus in
PR descriptions — matches how the other peer-parity fixtures describe
their own divergence records.
…ared helper (Copilot review)

Extract a new `getStaticAttrValue` util that resolves literal-valued
mustaches (`{{"foo"}}`, `{{true}}`, `{{-1}}`) and single-part concat
statements (`"{{true}}"`) to their static string value. `hasNonEmptyTextAttr`
now delegates to the helper — `aria-label={{""}}` / `aria-label="{{""}}"`
normalise to the empty string and flag the same as the text-node equivalent;
genuinely dynamic values (PathExpressions, multi-part concat) still
short-circuit to "assume truthy". Closes the bypass where authors wrapped
an empty accessible name in mustaches.

Byte-identical carrier of lib/utils/static-attr-value.js across all PRs
that land it.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…op audit fixture

Upstream maintainers don't want the per-PR `tests/audit/peer-parity`
pattern. Port one case that pinned distinct behavior:
- `<img alt=" " />` as VALID — whitespace-only alt is currently
  treated as decorative; jsx-a11y agrees.

All other audit cases were already covered by the regular tests on
this branch (extensive existing coverage of object/area/input variants,
empty aria-label/labelledby, presentation-role conflicts, etc).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants