NFR-A11Y-3 was argued in comments beside the rating strip, the
pick/reject mark and the focus-peaking chips, and nothing would have
failed if an edit made a set star differ from an unset one only in tint.
Only the clipping readout had a test.
These read the markup, as the accessibility-name tests do, since a
rendered window cannot be asked what a colour-blind reader sees. The
star and the flag must choose their glyph from their state, and the
glyphs they choose must be different drawings in icons.slint. The peaking
chips must be distinct words that reach the screen as text. Each colour
label must carry its own letter and the name the catalog's code stands
for, the mark must draw the letter, and the grid cell, the filter chips
and develop must draw the mark or the name. Breaking any of these by
hand makes the matching test fail.
Zooming the develop view changed the exported file. `Framing::view` is
kept out of the sidecar, out of `is_active` and out of `output_size`
precisely so that it cannot — but those exclusions keep it out of the
*edit*, and an export is a *render*. `visible_rect` deliberately folds
the view into the single rect the fused shader's prologue samples, so
`render_for_export` inherited it: at 4:1 it wrote the middle of the
frame, magnified to fill the file at the full output size, with the
detail kernels scaled four times over because `render_scale` folds the
view in as well. `render_thumbnail` did the same to the grid.
`render_uncropped` already suspends the view for this exact reason, so
the fix is its pattern: one `render_the_file` that both file-producing
paths go through, composing inside the suspension since the view
reaches the shader as a uniform baked at composition time. Restored
whatever happens — leaving the graph un-zoomed after a failed export
would throw away where the photographer was looking.
Nothing caught it because the guard checked the wrong things.
`zooming_does_not_change_the_exported_image` asserted the output size
and the crop; both held perfectly throughout. Renamed to
`zooming_does_not_change_the_size_or_the_crop`, which is what it
tests, and the pixels are now guarded where pixels exist. The new test
uses a ramp rather than quadrants deliberately: a four-quadrant frame
is self-similar under a centred zoom, and the first version of this
test passed against the bug because of it.
Traces FR-EXP-9, which asks for the full-quality pipeline "regardless
of what the display was showing".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sets()` recognised `name:` and `name =>` and not `name(arg) =>`, which is how
Slint writes a callback handler with a parameter. There is exactly one of those
among the properties asserted — `accessible-action-set-value`, the action a
screen reader uses to type a number into a slider — so the test reported
`SliderTrack` as missing the action it declares four lines above.
A false alarm rather than a false pass, and the less dangerous of the two. It
is still worth fixing rather than dropping the assertion: set-value is the
action that makes a slider reachable without dragging, which is most of what
the actions were added for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The whole interface carried five accessible-* declarations in 14,482 lines of
markup, all of them on a single row of the colour mixer, and nothing said so.
Every other control — every button, every tick-box, every chip, and the slider
the develop panel builds thirty-six of for the mixer alone — reached AT-SPI
and TalkBack as an unnamed rectangle. NFR-A11Y-2 is not a polish item for a
user in that position; it is whether the application can be used at all.
The annotations go on the shared components rather than on the screens, which
is the same argument widgets.slint was written to make one layer down: a
control named where it is used is a control unnamed everywhere it is used
next. Twelve components now declare a role, a name and — where the control
does something — the action assistive technology invokes to do it. Three
screens were touched, and only where the component could not know the answer.
SliderTrack is the one that mattered most and the one that could not be fixed
from inside itself. It is handed four numbers and knows nothing about what
they mean, so it takes a `label` and a formatted `readout` and every wrapper
passes down what it was already drawing. A test asserts that every
instantiation does, because a track added without one announces "slider, 0.35"
and looks perfectly correct in a screenshot.
It also gains increment, decrement and set-value. A slider that can only be
dragged is a slider a pointer is a modifier for, which is the objection
ui-navigation D-N2 makes about hover-only affordances with the argument run
one step further; these three are what a screen reader drives a slider with,
and they commit as well as change — one nudge is a whole gesture, so a caller
that persists on `committed` must hear about it.
Two decisions worth recording because the obvious alternative is wrong:
`active` on Button and IconButton is deliberately not announced from the
component. It says a toggle is on and says nothing about whether a control
that is *off* is a toggle at all, so announcing it would report every button
in the application as an unpressed toggle. The four call sites that mean a
toggle say so themselves, which Slint permits because the role is inherited.
SwatchSlider's row-level role is removed rather than kept. It was the one
control that had a label, and now that the track underneath it has one too the
two would nest — a slider inside a slider, the outer holding the value and the
inner holding the actions that can change it. The row stands down and hands
the same strings to the control that owns the gesture.
The test reads the markup the way darkroom-android's manifest test reads its
XML: there is no accessibility tree without a window, so what it defends is
the failure that actually happens — a role or a name lost in a refactor, which
compiles, renders identically, and is invisible to everyone not using a screen
reader.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
FR-DEV-3a is the property the declarative pipeline rests on: a node in
`ops/` is one file because nothing in `ui/` has to learn about it. It was
true — all fifteen ids grepped across `ui/` yield one hit, a localisation
test — and it was held by discipline alone.
That is the wrong mechanism for it. The failure is silent and cumulative:
special-casing one operation to fix a layout problem is defensible on its
own, and by the fifth the panel names half the chain and "a new operation is
one file" has stopped being true without any single commit having broken it.
Nothing would have told us.
The ids are read from `ops/*.yaml` rather than listed, so a node added
tomorrow is covered without anyone remembering this file — the same reason
`traceability` parses its denominators from `requirements.md` at run time.
Two decisions worth recording, because both are the difference between a
test that holds and one that gets deleted:
**Only string literals count.** `texture`, `contrast` and `clarity` are also
ordinary graphics and English terms, and `texture` appears throughout `dr-ui`
meaning a GPU texture. Matching bare words would fail constantly for reasons
unrelated to the invariant.
**`#[cfg(test)]` items are exempt, and finding them needs more care than it
looks.** The first version cut each file at the first textual match of
`#[cfg(test)]`, which in `develop.rs` is a *doc comment discussing the
attribute* at line 969 — it read 18% of the most important file in the scan
and passed. It now matches the attribute only as a whole line and skips the
item by brace depth, and `MIN_SHIPPING_FRACTION` fails the test outright if
the scan ever swallows the file again. The dangerous failure here is not a
false alarm, which someone investigates; it is examining nothing and
reporting success.
Verified both ways: it passes on the tree, and an `"exposure"` planted at
develop.rs:3635 — past two `#[cfg(test)]` attributes, exactly where the
first version was blind — fails with the file, the line and the reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>