fix(gate-35): give NewsWidget thumbnails a real text alternative - #127
fix(gate-35): give NewsWidget thumbnails a real text alternative#127rubenvdlinde wants to merge 2 commits into
Conversation
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.
Quality Report — ConductionNL/launchpad @
|
| 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.
|
S33: this PR is now SUPERSEDED — recommend CLOSE, not merge. It went I merged
That exemption is the a11y argument too: inside a link, So 🔴 For the record: launchpad 📌 Follow-up worth having, deliberately not done here to avoid widening scope:
|
Quality Report — ConductionNL/launchpad @
|
| 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.
|
Closing: superseded by Recorded for whoever revisits it: the gate's own checker ( |
gate-35 img-alt-empty-only: 2 -> 0
Both
<img class="news-widget__thumb">inNewsWidget.vueshippedalt=""—a positive claim that the image is decorative and that assistive tech should
ignore it. It is not decorative:
thumbnailUrlis 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-altwith an empty altinstead 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, whichgate-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 whatthe 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:thumbnailcarries no description of its own, so the mosthonest alternative available is to say what the image is and which article it
belongs to:
Thumbnail for "<title>", falling back toArticle thumbnailwhenthe 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
altattribute off the rendered<img>, because amethod 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/developmentand thenew tests kept, all three fail with
expected '' to be …. They exercise thedefect 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.
cc6858e8[gate-35] img-alt-empty-only<img alt="">on semantic-bound srcVerdict-by-verdict diff:
gate-35FAIL→PASS, nothing else regressed.gate-16and
gate-47appear only on the branch because they are delta gates and thebase run has
HEAD == base(correctly NOT APPLICABLE, not PASS).gate-31 img-altwas already PASS and stays PASS.Frontend checks
NewsWidget.spec.js: 7 → 10 tests, all passingthe same tree reports 3 errors, so
rc=0on the real file is a real verdict,not a run that did nothing. Probe deleted;
git statusconfirms it leftnothing behind.
--check: cleanNot claimed
Hydra Gatesis red on this repo's base run for other reasons (gate-19e2e-coverage,
gate-22,gate-26,gate-53,gate-57— 6 failing gatesremain after this PR). This PR closes one of them and does not touch the rest.