ENG-1745: Test 22 False Negative on A Elements with role=link

Bug (ENG-1745): Access Engine test 22 (an A element must not contain nested interactive content) scoped its candidate set to a[data-ae_vis][data-ae_ar="null"] — anchors with no ARIA role at all. Any role attribute took the anchor out of scope, including role="link", which does not change what the element is. Markup such as <a href="foo" role="link">Testing this out<button>bad</button></a> was silently skipped: the nested button went unreported — a false negative.

Impact: Real nested-interactive violations were missed on any link that spells out its role. This is common in design systems and framework component libraries, which frequently emit a redundant role="link" on anchors.

Test Logic (test 22):

Common Scenarios:

Fix: Extend the candidate set with the computed-role attribute added in ENG-1730 (data-ae_computed_role, the explicit-or-implicit ARIA role):

a[data-ae_vis][data-ae_ar="null"], a[data-ae_vis][data-ae_computed_role="link"]

The computed-role branch matches role="link" as well as case variations and multi-token role lists whose first valid token is link (role="foo link"), mirroring browser behaviour. The original data-ae_ar="null" branch is retained so a role-less A without an href (implicit role "") stays in scope exactly as before — the change only adds coverage, it never removes it.
N/A — out of scope
PASS — not flagged (correct)
FAIL — flagged (correct; the new cases are the ENG-1745 repro, the rest are regression guards)

N/A Tests — Test 22 Does Not Apply

Anchor with role="button" containing a button other role

something

The anchor is exposed as a button, not a link, so test 22 does not apply. Only role=link (or no role) is in scope. Should not be flagged.

Anchor with role="presentation" containing a button other role

something

An explicit presentational role removes the link semantics the test is about. Out of scope, unchanged by ENG-1745. Should not be flagged.

Anchor with role="link" hidden with display:none hidden

(Anchor is present but hidden via display:none.)

Test 22 only evaluates elements that are not intentionally hidden: the selector requires [data-ae_vis], and a display:none element is marked data-ae_invis instead. Should not be flagged.

PASS Tests — In Scope, No Violation

Baseline: role-less anchor with text only no role

something

In scope, contains no interactive element. Should not be flagged.

Anchor with role="link" and text only role=link

something

Newly in scope after ENG-1745, but there is nothing nested to report. Should not be flagged.

Anchor with role="link" containing input type="hidden" role=link

something

A hidden input is not an interactive control; the test explicitly excludes type=hidden. That exclusion applies on the new role=link branch too. Should not be flagged.

Anchor with role="link" containing a button that has its own ARIA role role=link

something

Nested elements with an assigned ARIA role are out of the test's descendant set, both before and after ENG-1745. Should not be flagged.

FAIL Tests — Correctly Flagged

ENG-1745 repro cases — missed before the fix

Ticket repro: role="link" anchor containing a button role=link

Testing this out

The exact markup from ENG-1745. Pre-fix: not flagged ❌. Post-fix: flagged ✓.

role="link" anchor containing a text input role=link

something

Pre-fix: not flagged ❌. Post-fix: flagged ✓.

role="link" anchor containing a select role=link

something

Pre-fix: not flagged ❌. Post-fix: flagged ✓.

role="link" anchor containing a textarea role=link

something

Pre-fix: not flagged ❌. Post-fix: flagged ✓.

Uppercase role="LINK" anchor containing a button case

something

ARIA role tokens are case-insensitive and the computed role is normalised to lower case. Pre-fix: not flagged ❌. Post-fix: flagged ✓.

Multi-token role="foo link" anchor containing a button token list

something

Browsers skip unknown tokens and use the first valid one, so this element is a link. The computed role resolves to link even though the recorded data-ae_ar is foo. Pre-fix: not flagged ❌. Post-fix: flagged ✓. (Tests 37 and 412 also flag this element for the invalid foo token — that is expected and unrelated to test 22.)

role="link" anchor with no href containing an input no href

something

Without href the anchor has no implicit link role, but the explicit role="link" makes it one. Pre-fix: not flagged ❌. Post-fix: flagged ✓.

Regression guards — flagged before and after the fix

Role-less anchor containing a button no role

something

The original test 22 case. Must stay flagged ✓.

Role-less anchor with no href containing a button no role, no href

something

Its computed role is empty (no href), so it is only in scope through the retained data-ae_ar="null" branch. This case guards against narrowing the selector to the computed role alone. Must stay flagged ✓.

Real-World Scenarios

Product card wrapped in a role="link" anchor with an "Add to cart" button role=link

Antelope print £24.00

Card-as-link pattern: the button is unreachable as a separate control inside the link. Pre-fix: not flagged ❌. Post-fix: flagged ✓.

Design-system navigation link emitting a redundant role="link" around a filter select role=link

Browse

Component libraries routinely add a redundant role="link" to their anchor components, which used to hide every nested-control violation they contained. Pre-fix: not flagged ❌. Post-fix: flagged ✓.