fix(booth): release was painting over the delete ×, which was unclickable
Operator: "release button covers delete button". Measured before touching
anything: release 58x24 at (323,266), × 30x30 at (349,268) — 30x22 px of
overlap on a 30px button, and `elementFromPoint` at the ×'s centre returned the
release form. The × I added yesterday was 100% unclickable from the moment it
shipped.
Cause: both were `position:absolute` on the same corner, each with its own
guessed offset, and `release` is the later sibling so it won. Replaced with one
flex row positioned once — release left, × right at the card corner where the
ephemeral lane's × already lives, so muscle memory transfers and neither can
drift back on top of the other when a label changes width.
Verified by measurement, not inspection: overlap 0 px, and clicks at each
control's centre now land on that control. The ephemeral lane's × and ★ were
re-checked and are unaffected.
ADDS scripts/layout-probe.py, because markup inspection STRUCTURALLY cannot see
this and I have now shipped two dead controls in two days by reading templates
instead of rendering them. It asks a real browser what a click at each
control's centre would hit.
It took four iterations to become trustworthy and the failures are the point:
1. `top.contains(el)` counted an ANCESTOR overlay as a hit -- the exact case
it exists to catch. Version one reported OK for a real overlay.
2. elementFromPoint is viewport-relative, so everything below the fold read
as occluded and buried the real findings.
3. getBoundingClientRect() on a WRAPPED INLINE element is the union of its
line boxes, whose centre lands in the gutter between them -- three zip
links reported occluded by their own parent. Real geometry, wrong question.
Only the fourth version fires on a genuine overlay while staying silent on the
clean page. Both controls were run; a probe never seen to fail proves nothing.
173 tests pass.
This commit is contained in:
@@ -39,6 +39,35 @@ rsync -a ./out/ nh3-dev:booth-data/my-run/
|
||||
|
||||
Then hand the operator `http://10.100.10.50:8090/b/my-run/`.
|
||||
|
||||
## Checking that controls can actually be clicked
|
||||
|
||||
```bash
|
||||
<a python with playwright> scripts/layout-probe.py [URL ...]
|
||||
```
|
||||
|
||||
⚠ **Markup inspection structurally cannot catch occlusion, and this UI has
|
||||
shipped two dead controls in two days** — a reveal button whose handler Jinja
|
||||
discarded, and a `×` that a sibling `release` form painted over completely
|
||||
(30x22 px overlap on a 30px button; `elementFromPoint` at its centre returned
|
||||
the other form). Both were reported by the operator. Both passed every test,
|
||||
because the markup, the routes and the CSS were each individually correct.
|
||||
|
||||
The probe walks every button and link, scrolls it into view, and asks the
|
||||
browser what a click at its centre would actually hit. It took four iterations
|
||||
to become trustworthy, and each failure is worth knowing because they are the
|
||||
traps in writing this kind of check at all:
|
||||
|
||||
1. `top.contains(el)` counted an **ancestor** overlay as a hit — which is the
|
||||
exact case the probe exists to catch. It reported OK for a real overlay.
|
||||
2. `elementFromPoint` is **viewport-relative**, so everything below the fold
|
||||
read as occluded. Scroll first.
|
||||
3. `getBoundingClientRect()` on a **wrapped inline** element is the union of
|
||||
its line boxes, whose centre can sit in the gutter between them, on the
|
||||
parent. Use `getClientRects()[0]`.
|
||||
4. Only after all three does the positive control (a real overlay) fire while
|
||||
the negative control (the clean page) stays silent. **Both were run.** A
|
||||
probe that has never been seen to fail is not evidence of anything.
|
||||
|
||||
## Blurring an item (cosmetic censoring)
|
||||
|
||||
⚠⚠ **Blur is NOT access control.** A blurred item is still served at its own
|
||||
|
||||
Reference in New Issue
Block a user