Skip to content

fix(gate-35): give NewsWidget thumbnails a real text alternative - #127

Closed
rubenvdlinde wants to merge 2 commits into
developmentfrom
S2/gate-fixes
Closed

fix(gate-35): give NewsWidget thumbnails a real text alternative#127
rubenvdlinde wants to merge 2 commits into
developmentfrom
S2/gate-fixes

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

gate-35 img-alt-empty-only: 2 -> 0

Both <img class="news-widget__thumb"> in NewsWidget.vue shipped alt=""
a positive claim that the image is decorative and that assistive tech should
ignore it. It is not decorative: thumbnailUrl is the article's own lead image,
chosen by the publisher, and a sighted reader gets it for free. That is exactly
the shape gate-35 exists to catch: silencing gate-31 img-alt with an empty alt
instead of naming the image (WCAG 2.2 AA SC 1.1.1 Non-text Content).

How it got here — worth knowing fleet-wide

The component was written with :alt="''" — a bound empty alt, which
gate-35 deliberately does not judge (a bound value is reviewer judgement). An
eslint 10 autofix in bff6092a "migrate to eslint 10 +
@nextcloud/eslint-config 9"
rewrote it to the literal alt="", which is what
the gate reads.

So the finding is real and the autofix is what surfaced it — a lint autofix
changed the meaning of the attribute, not just its spelling.

The alt text

RSS/Atom media:thumbnail carries no description of its own, so the most
honest alternative available is to say what the image is and which article it
belongs to: Thumbnail for "<title>", falling back to Article thumbnail when
the item has no title — a nameless image gets announced by its filename, which
is worse than a generic but true description. Both strings go through
t('launchpad', …).

Tests assert the DOM, not the helper

Three tests read the alt attribute off the rendered <img>, because a
method returning good text is worth nothing if the template never binds it.
Both render paths are covered — the linked <a> item and the inert <div>
item, which carried separate alt=""s.

Positive-controlled: with the component reverted to origin/development and the
new tests kept, all three fail with expected '' to be …. They exercise the
defect rather than describing it.

Evidence — the gate's own script

ConductionNL/.github@main, hydra-gates/scripts/run-hydra-gates.sh --full
(the version CI runs). Base measured in a clone pinned to the exact base sha.

base cc6858e8 this branch
[gate-35] img-alt-empty-only FAIL — 2 <img alt=""> on semantic-bound src PASS
markup files inspected 85 85
gates reporting 71 71
failing gates (exit code) 7 6

Verdict-by-verdict diff: gate-35 FAIL→PASS, nothing else regressed. gate-16
and gate-47 appear only on the branch because they are delta gates and the
base run has HEAD == base (correctly NOT APPLICABLE, not PASS).

gate-31 img-alt was already PASS and stays PASS.

Frontend checks

  • vitest NewsWidget.spec.js: 7 → 10 tests, all passing
  • eslint: clean. Positive-controlled — a deliberately broken probe component in
    the same tree reports 3 errors, so rc=0 on the real file is a real verdict,
    not a run that did nothing. Probe deleted; git status confirms it left
    nothing behind.
  • prettier --check: clean

Not claimed

Hydra Gates is red on this repo's base run for other reasons (gate-19
e2e-coverage, gate-22, gate-26, gate-53, gate-57 — 6 failing gates
remain after this PR). This PR closes one of them and does not touch the rest.

Both `<img class="news-widget__thumb">` in NewsWidget shipped `alt=""`,
which is a claim that the image is decorative and that assistive tech
should ignore it. It is not decorative: `thumbnailUrl` is the article's
own lead image, chosen by the publisher, and a sighted reader gets it for
free. That is the shape gate-35 exists to catch — silencing gate-31
img-alt with an empty alt rather than naming the image (WCAG 2.2 AA
SC 1.1.1).

HOW IT GOT HERE, because it is worth knowing fleet-wide: the component was
written with `:alt="''"` — a BOUND empty alt, which gate-35 deliberately
does not judge. An eslint 10 autofix (bff6092, "migrate to eslint 10 +
@nextcloud/eslint-config 9") rewrote it to the literal `alt=""`, and that
is what the gate reads. The finding is real, and the autofix is what
surfaced it.

RSS/Atom `media:thumbnail` carries no description of its own, so the best
honest alternative available is to say what the image is and which article
it belongs to: `Thumbnail for "<title>"`, falling back to
`Article thumbnail` when the item has no title — a nameless image is
announced by its filename, which is worse than a generic but true one.

Three tests assert the attribute THAT REACHES THE DOM, not merely that the
helper returns good text: a method producing a fine string is worth
nothing if the template never binds it. Both render paths are covered (the
linked `<a>` item and the inert `<div>` item, which carried separate
`alt=""`s). Verified as a positive control: all three FAIL against the
unfixed component with `expected '' to be ...`.

Measured with the gate's own script (ConductionNL/.github@main,
`scripts/run-hydra-gates.sh --full`), over 85 markup files in scope:

  before  [gate-35] img-alt-empty-only: FAIL — 2 <img alt=""> on semantic-bound src
  after   [gate-35] img-alt-empty-only: PASS

Repo failing-gate count 7 -> 6; no other gate verdict changed.
NewsWidget vitest 7 -> 10 tests, all passing; eslint and prettier clean.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ 66f24f0

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
format
composer ✅ 104/104
npm ✅ 525/525
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-16 10:46 UTC

Download the full PDF report from the workflow artifacts.

Resolved the one conflict in NewsWidget.vue in favour of `development`, and
reverted this branch's other change to the same file, because `development`
has ALREADY closed gate-35 here — independently, and better.

`ef50f7da` ("fix(gates): clear gate-26, gate-35 and gate-57") landed on
`development` while this PR sat, and launchpad's `development` is now fully
green: run 31949946649 (event=push, head 8f1949f) is 35 jobs, zero cancelled,
zero failures, `Hydra Gates` = success. So gate-35 is closed with or without
this branch.

Measured with the gate's own checker rather than argued
(ConductionNL/.github@main `scripts/lib/check_markup_a11y.py`,
`--rule img-alt-empty-only`, one markup file, with a positive control so the
zero is a verdict and not a silent import failure):

  pre-fix NewsWidget.vue   1 finding  (line 77, the INERT card's <img>)
  this branch's version    0 findings
  development's version    0 findings

Note the pre-fix count is ONE, not the two this branch's commit message
recorded. The checker has since gained `_named_by_context()`, which exempts an
`<img alt="">` whose name-giving ancestor already carries text. The LINKED
card's thumbnail sits inside an `<a>` that also contains the headline, so it
was never the finding — only the inert card's, which is inside a plain `<div>`.

That exemption is also the accessibility argument. Inside a link that already
contains the headline, the image's `alt` is CONCATENATED into the link's
accessible name, so `:alt="thumbnailAlt(item)"` makes the anchor announce
'Thumbnail for "Foo". Foo. <summary>…' — the redundancy WCAG H67 exists to
avoid. `alt=""` there is correct, not lazy. `development`'s
`alt="" aria-hidden="true"` on the inert card states decorativeness in the
platform's own vocabulary, which is exactly what the gate's
`_DECORATIVE_DECLARED` recognises and what a screen reader honours.

So `thumbnailAlt()` and its three tests are reverted with the markup they
asserted; nothing in `src/` references it. This branch's tree is now
byte-identical to `origin/development`.

⚠️ This PR therefore has NO remaining diff and should be CLOSED as superseded,
not merged. Its `Branch Policy Check` failure is an artefact of this
programme's `S2/`-style branch naming and was never in scope here.
@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

S33: this PR is now SUPERSEDED — recommend CLOSE, not merge. changedFiles is 0.

It went dirty on ef50f7da ("fix(gates): clear gate-26, gate-35 and gate-57"), not on #126development fixed launchpad's gate-35 independently, in the same file, while this sat open.

I merged development in and resolved in favour of development, because its fix is better and this one's premise has expired. Measured with the gate's own checker (.github@main check_markup_a11y.py --rule img-alt-empty-only), positive control included:

tree findings
pre-fix NewsWidget.vue 1 (line 77 — the INERT card)
this branch 0
development 0

⚠️ One, not the two this branch's commit recorded. The checker has since gained _named_by_context(), which exempts an <img alt=""> whose name-giving ancestor already carries text. The linked card's thumbnail is inside an <a> that also contains the headline, so it was never a finding.

That exemption is the a11y argument too: inside a link, alt is concatenated into the link's accessible name, so :alt="thumbnailAlt(item)" would make the anchor announce Thumbnail for "Foo". Foo. … — the redundancy WCAG H67 exists to avoid. development's alt="" aria-hidden="true" on the inert card is what the gate's _DECORATIVE_DECLARED recognises and what every AT honours.

So thumbnailAlt() and its three tests are reverted with the markup they asserted (grep -rn thumbnailAlt src/ → nothing). The tree is byte-identical to origin/development.

🔴 For the record: launchpad development is fully green — run 31949946649 (event=push, 8f1949f1): 35 jobs, zero cancelled, zero failures, Hydra Gates = success.

📌 Follow-up worth having, deliberately not done here to avoid widening scope: development's fix has no test asserting aria-hidden="true" reaches the DOM, so a future autofix could drop it and only the gate would notice.

⚠️ Branch Policy Check fails on the S2/ branch name. That is this programme's naming convention, not a defect in the work, and no app-side change fixes it.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/launchpad @ 13d7741

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
format
composer ✅ 104/104
npm ✅ 525/525
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-16 14:39 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

Closing: superseded by ef50f7da on development, which fixed launchpad's gate-35 independently in the same file while this PR was open. After merging development in, this branch has changed_files: 0 — there is nothing left to land.

Recorded for whoever revisits it: the gate's own checker (.github@main, with a positive control) reports 1 finding on the pre-fix file, not the 2 this PR's commit message recorded. check_markup_a11y.py has since gained _named_by_context(), which exempts an <img alt=""> whose name-giving ancestor already carries text — so the linked card's thumbnail was never a finding. That exemption is also the a11y argument: inside a link, alt is concatenated into the link's accessible name, so setting it there would announce the headline twice. On that card this PR was a small regression aimed at a finding that no longer exists.

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.

1 participant