From d4f64fd7ec589366f9ed433f1300d6ae36b5b56c Mon Sep 17 00:00:00 2001 From: Vuong Hoang Date: Mon, 28 Sep 2026 11:20:56 -0700 Subject: [PATCH] fix(as-S5a): every control named, one h1 and a skip link, rings and hit areas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The markup and CSS half of the anti-slop interaction work. The in-place client is untouched (that is S5b). - Glyph-only controls carry a name: the withdraw ×s, downloads, open full page, the viewers' ✕, the board's pin, copy and remove, the bench's remove, the 1:1 toggle ("1:1, natural pixels"). Film-strip and tray frames carry the file's name as sr-only text instead of reading "01". A Desk row's wipe names its booth. - Fields are named by aria-label, not by their placeholder. - The inline ask's options are a radiogroup labelled by the prompt; a single-question fieldset gets an sr-only legend; a titled ask's title takes bk-ask--title (it duplicated the question's id). - One h1 per page (sr-only on the Desk, review and compare), a skip link to
, theme-color for light and dark. - The review tape is one picture (role=img); its segments leave the tab order (the film strip holds the same links, named). - Wipe now uses the Desk's delegated prompt, moved to base.html: it names the booth and asks the kept-booth question for a kept booth. - Embed focus rings of its own; rings drawn inside clipping containers; the withdraw × at least 24px, 44px under a coarse pointer; touch-action:manipulation; strips contain their overscroll; a long slug wraps on a phone. - A truncated why carries its full text in title; a countdown of 48h or more reads in days. Two r2_flow.toml rows for the confirm helper now name base.html, where the helper moved (anchors unchanged; the gate found them drifted). Contract: as_antislop S5a. Falsifiers: antislop.toml 86/86 proved (S1-S6, S5a); all 12 tables 366/366 proved on this tree. --- booth/app.py | 5 + booth/static/embed.js | 5 + booth/templates/_ask_inline.html | 10 +- booth/templates/_marks.html | 16 +- booth/templates/_provenance.html | 2 +- booth/templates/base.html | 63 +++++- booth/templates/booth.html | 24 +-- booth/templates/compare.html | 7 +- booth/templates/doc.html | 4 +- booth/templates/index.html | 40 +--- booth/templates/view.html | 19 +- docs/contracts/as_antislop.contract.md | 40 +++- tests/mutations/antislop.toml | 268 +++++++++++++++++++++++++ tests/mutations/r2_flow.toml | 4 +- tests/test_antislop.py | 191 ++++++++++++++++++ tests/test_antislop_browser.py | 59 ++++++ 16 files changed, 675 insertions(+), 82 deletions(-) diff --git a/booth/app.py b/booth/app.py index 46a776b..c547790 100644 --- a/booth/app.py +++ b/booth/app.py @@ -231,6 +231,11 @@ def human_dur(seconds: float) -> str: return "expired" h, rem = divmod(s, 3600) m, _ = divmod(rem, 60) + # as S5a: past two days the hours roll up ("6d 23h", not "167h 12m"); the + # minutes are noise at that range. Under 48h the hours stay, as before. + if h >= 48: + d, h = divmod(h, 24) + return f"{d}d {h}h" if h else f"{d}d" if h and m: return f"{h}h {m}m" if h: diff --git a/booth/static/embed.js b/booth/static/embed.js index 31e3e66..4c93c2a 100644 --- a/booth/static/embed.js +++ b/booth/static/embed.js @@ -47,6 +47,9 @@ "color:#dce3e5;background:rgba(12,16,20,.86);border:1px solid rgba(255,255,255,.16);", "-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);transition:background .12s,border-color .12s}", ".booth-nav-home:hover{background:rgba(31,35,40,.96);border-color:rgba(255,255,255,.34)}", + /* as S5a: our own focus rings, so a host page's `outline:none` cannot take + them. The chips sit on any host: a white ring inside a dark halo reads on both. */ + ".booth-nav-home:focus-visible,.booth-nav-asks:focus-visible{outline:2px solid #fff;outline-offset:1px;box-shadow:0 0 0 4px #15191d}", /* amber = needs you: the one chip that asks to be clicked */ ".booth-nav-asks{right:7.4rem;font:600 13px/1.25 'IBM Plex Sans',ui-sans-serif,system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;", "color:#15191d;background:#fbc10f;border:1px solid #fbc10f;transition:filter .12s}", @@ -102,6 +105,8 @@ "padding:.7rem 1.1rem;border-radius:8px;border:1px solid var(--bk-accent);", "background:var(--bk-accent);color:var(--bk-on-accent)}", ".bk-ask-go:hover{filter:brightness(1.06)}", + ".bk-ask-go:focus-visible{outline:2px solid var(--bk-accent);outline-offset:2px}", + ".bk-ask-opt:has(input:focus-visible){outline:2px solid var(--bk-accent);outline-offset:1px}", ".bk-ask-was{margin:.15rem 0 .55rem;font-size:.86rem}", ".bk-ask-err{color:var(--bk-err);font-size:.86rem}", /* [hidden] restated at class specificity: a host rule as plain as diff --git a/booth/templates/_ask_inline.html b/booth/templates/_ask_inline.html index c9ef9a3..e51646b 100644 --- a/booth/templates/_ask_inline.html +++ b/booth/templates/_ask_inline.html @@ -25,10 +25,10 @@ {% set skipped = a.answer and not picked %}
{% if picked %}✓ answered{% elif skipped %}— skipped{% else %}? your pick{% endif %} -

{{ q.prompt }}

+

{{ q.prompt }}

{% if picked %}

recorded: {{ qa.label }}{% if qa.notes %} — {{ qa.notes }}{% endif %}

{% elif skipped %}

left blank — pick one any time, or leave it{% if qa and qa.notes %}; note: {{ qa.notes }}{% endif %}

{% endif %} -
+
{% for o in q.options %}
{% endmacro %} @@ -59,7 +59,7 @@ {% if not a.answer %}

Answer what you can — blanks are fine, and you can come back.

{% endif %} {% if a.notes_enabled %} + aria-label="{{ a.notes_label }}" placeholder="{{ a.notes_label }} (optional)">{{ a.answer.notes if a.answer else '' }} {% endif %} {# Empty and hidden: where embed.js says which asks a several-at-once submit @@ -74,7 +74,7 @@
⚠ broken ask

this question could not be read: {{ a.error }}

{% else %} - {% if a.title %}

{{ a.title }}

{% endif %} + {% if a.title %}

{{ a.title }}

{% endif %} {% for q in a.questions %}{{ question(a, q, form_id, name_url) }}{% endfor %} {{ submit(a, form_id, name_url) }} {% endif %} diff --git a/booth/templates/_marks.html b/booth/templates/_marks.html index 272ea1e..ad96f02 100644 --- a/booth/templates/_marks.html +++ b/booth/templates/_marks.html @@ -40,7 +40,7 @@
{% if marks_page %}{% endif %} - +

This mark could not be read: {{ a.error }}

@@ -94,7 +94,7 @@ {% set field = 'choice.' ~ q.key if a.multi else 'choice' %} {% set qa = a.answer.answers.get(q.key) if (a.answer and a.multi) else a.answer %}
- {% if a.multi %}{{ loop.index }}. {{ q.prompt }}{% endif %} + {% if a.multi %}{{ loop.index }}. {{ q.prompt }}{% else %}{{ q.prompt }}{% endif %}
{% for o in q.options %}
{% if q.notes %} - + {% endif %}
{% endfor %} {% if a.notes_enabled %} - + {% endif %}
@@ -145,7 +145,7 @@ @@ -163,7 +163,7 @@
  • {{ m.target }}
    - +
  • {% endfor %} @@ -196,7 +196,7 @@
    {% if marks_page %}{% endif %} - +
    {{ a.text }}
    @@ -209,7 +209,7 @@ all — this is the direction that was running through chat. #}
    {% if marks_page %}{% endif %} - +
    {% endif %} diff --git a/booth/templates/_provenance.html b/booth/templates/_provenance.html index c702093..8c7326a 100644 --- a/booth/templates/_provenance.html +++ b/booth/templates/_provenance.html @@ -12,6 +12,6 @@ {% elif m.error %}
    unreadable
    {% else %} -
    {{ m.handle }}{% if m.why %} · {{ m.why }}{% endif %}
    +
    {{ m.handle }}{% if m.why %} · {{ m.why }}{% endif %}
    {% endif %} {%- endmacro %} diff --git a/booth/templates/base.html b/booth/templates/base.html index 0e3e90e..02677ee 100644 --- a/booth/templates/base.html +++ b/booth/templates/base.html @@ -3,6 +3,8 @@ + + {# r2b: per-browser state applied BEFORE FIRST PAINT, so a revealed booth does not flash blurred on the next page of the reel. Every change of page is a full load (the in-place client re-fetches the current URL and never @@ -1059,9 +1061,28 @@ @media (prefers-reduced-motion: reduce){ *{transition:none !important} } + + /* ---- as S5a: accessibility plumbing ---------------------------------- */ + .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden; + clip:rect(0 0 0 0);white-space:nowrap;border:0} + .skip-link{position:absolute;left:12px;top:8px;z-index:2147483647;transform:translateY(-200%); + padding:8px 12px;border-radius:var(--radius-md);background:var(--surface-raised); + color:var(--text-heading);border:1px solid var(--border-strong);font-weight:600} + .skip-link:focus-visible{transform:none} + /* a ring drawn OUTSIDE is clipped by these overflow:hidden containers: draw it inside */ + .theme button:focus-visible,.vtoggle button:focus-visible,.item a:focus-visible, + .desk-panel a:focus-visible{outline-offset:-2px} + /* hit areas: at least 24px anywhere, 44px under a finger */ + .mark-x{min-width:24px;min-height:24px} + @media (pointer:coarse){.mark-x,.flagtoggle button,.blurtoggle button,.board-check, + .board-selall input{min-width:44px;min-height:44px}} + a,button,summary,label,input,select,textarea{touch-action:manipulation} + .film,.vcap,.doc-body,.cmp-strip{overscroll-behavior:contain} + @media (max-width:600px){.h1-slug{white-space:normal;overflow-wrap:anywhere}} +
    The Booth held for review · wipes in {{ ttl_hours }}h unless kept @@ -1072,7 +1093,7 @@
    -
    +
    {# R2 C3: where the in-place script says it could not save in place. Server rendered and empty, so the script only ever sets its text — it builds no markup (INV-6). #} @@ -1475,5 +1496,45 @@
    drop a folder into {{ data_dir }}{% if host %} · {{ host }}{% endif %}
    + diff --git a/booth/templates/booth.html b/booth/templates/booth.html index bb4914b..5ed942c 100644 --- a/booth/templates/booth.html +++ b/booth/templates/booth.html @@ -48,7 +48,7 @@
    {{ m.text }}
    - +
    {% endfor %} @@ -56,7 +56,7 @@ + note
    - +
    @@ -123,7 +123,7 @@ {% endif %} {% if not board %}
    + data-booth="{{ name }}" data-confirm="{{ 'wipe-kept' if kept else 'wipe' }}">
    {% endif %} @@ -215,13 +215,13 @@ {% endif %} {% endfor %} + title="remove this bench" aria-label="remove the bench {{ b.name or b.url }}">×
    {% endfor %}
    - - + +
    @@ -263,7 +263,7 @@
    {% if e.safe %}{{ e.desc }} @@ -279,9 +279,9 @@ {% if e.who|byline %}{{ e.who|byline }}{% endif %} {% if e.when %}{% endif %}
    - +
    {% endfor %} @@ -383,8 +383,8 @@ {{ ordinal(it) }} {{ it.name }} - ⤢ - ⬇ + ⤢ + ⬇ {{ blurtoggle(name_url, it, 'doc-act') }} {{ markcontrols(name_url, it, item_marks.get(it.name, []), 'doc-act') }} @@ -437,7 +437,7 @@ {{ marknotes(name_url, it, item_marks.get(it.name, [])) }} {% else %}
    - ⬇ + ⬇ {{ it.caption or it.name }} {# R2: every MEDIA tile links into the review — a picture through its image, sound and video through this. Enter on the grid cursor diff --git a/booth/templates/compare.html b/booth/templates/compare.html index 69e2126..c6f975b 100644 --- a/booth/templates/compare.html +++ b/booth/templates/compare.html @@ -14,9 +14,10 @@ apply unchanged; `.compare` overrides its grid. #} {% macro num(n) -%}#{{ "%0*d"|format(ord_width, n) }}{%- endmacro %} {% block content %} +

    compare {{ sides.a.rel }} and {{ sides.b.rel }}

    - ✕ + ✕ compare {{ num(sides.a.ordinal) }} · {{ num(sides.b.ordinal) }} ‹‹ @@ -27,7 +28,7 @@ title="linked: ← and → move both sides (L)">{{ '⛓ linked' if linked else '⛓ unlinked' }} {% if any_image %} {% endif %} {% if film | selectattr('blurred') | list %}{% endif %} @@ -82,7 +83,7 @@ {% for x in film %} - {%- if x.kind == 'image' %}{% else %}{{ '♪' if x.kind == 'audio' else '▶' }}{% endif -%} + {{ x.name }}{%- if x.kind == 'image' %}{% else %}{{ '♪' if x.kind == 'audio' else '▶' }}{% endif -%} {{ num(x.ordinal) }} {%- if x.is_a or x.is_b %}{{ 'A' if x.is_a }}{{ 'B' if x.is_b }}{% endif -%} diff --git a/booth/templates/doc.html b/booth/templates/doc.html index f9985b5..f7374c9 100644 --- a/booth/templates/doc.html +++ b/booth/templates/doc.html @@ -4,13 +4,13 @@ {% block content %}
    - ✕ + ✕ {{ file }} {# Reveal all can lift this page's blur, so this page must be able to put it back (r2b, heid bug-hunt). #} {% if blurred %}{% endif %} - ⬇ + ⬇
    {# Same record, same reason as the image viewer: the sidecar that says what this doc IS travels with it to full-page view. #} diff --git a/booth/templates/index.html b/booth/templates/index.html index a883a52..325ecff 100644 --- a/booth/templates/index.html +++ b/booth/templates/index.html @@ -73,17 +73,18 @@
    + data-booth="{{ b.name }}" data-confirm="wipe-kept"> {%- else %}
    + data-booth="{{ b.name }}" data-confirm="wipe"> {%- endif %}
    {%- endmacro %} {% block content %} +

    Desk

    {% if needs %} @@ -192,40 +193,5 @@ }); })(); - /* Destructive-action confirmation, delegated and DATA-DRIVEN. The booth name - travels as a data attribute, where escaping is escaping, and never reaches - a JS string literal: a booth name is agent-authored, and an inline handler - carrying one was a live injection path. With JS off the form submits - without a prompt. */ - (function () { - var WORDS = { - release: function (n) { - return 'Release “' + n + '”?\n\nIt rejoins the sweep: it will be wiped ' - + '{{ ttl_hours|int }}h after its last activity. Nothing is deleted by this step.'; - }, - 'wipe-kept': function (n) { - return 'WIPE the KEPT booth “' + n + '”?\n\nThis deletes it and its files ' - + 'immediately. Kept booths are the ones nothing else will clean up, so nobody ' - + 'else is going to do this for you — and nothing brings it back.'; - }, - wipe: function (n) { return 'Wipe booth “' + n + '”?'; } - }; - /* A word the page does not know still ASKS: fail closed. Keying the - prompt on `word &&` meant a typo'd data-confirm submitted unguarded. */ - var ASK = function (n) { return 'Really do this to “' + n + '”?'; }; - /* The name as the operator should READ it. Escaping keeps it out of any - script, but a bidi override (U+202E) or a newline in an agent-made name - still rewrote the dialog's text — what is read before approving a wipe. - Controls and bidi formatting show as U+FFFD, visibly, never silently. */ - function shown(n) { - return n.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, '\ufffd'); - } - document.addEventListener('submit', function (ev) { - var form = ev.target.closest ? ev.target.closest('form[data-confirm]') : null; - if (!form) return; - var word = WORDS[form.getAttribute('data-confirm')] || ASK; - if (!confirm(word(shown(form.getAttribute('data-booth') || '')))) ev.preventDefault(); - }, true); - })(); {% endblock %} diff --git a/booth/templates/view.html b/booth/templates/view.html index 255a6eb..cd5043e 100644 --- a/booth/templates/view.html +++ b/booth/templates/view.html @@ -9,9 +9,10 @@ THE STAGE NEVER IS — swapping it would restart a playing track. #} {% macro num(n) -%}#{{ "%0*d"|format(ord_width, n) }}{%- endmacro %} {% block content %} +

    {{ file }}

    - ✕ + ✕ {{ num(ordinal) }} {{ file }} {# R3: this item against the next one in the ring, side by side (C). #} @@ -21,7 +22,7 @@ and only ever rendered for a picture. With scripts off the image shows at fit size and no judgment depends on this. #} {% endif %} {# r2b D2b + D2, in the top bar: outside every data-region, so no swap @@ -32,16 +33,16 @@ {% if film | selectattr('blurred') | list %}{% endif %} - ⬇ + ⬇
    {# THE TAPE (B's device): one segment per item in the review ring — seen, flagged, current — so how far through the set you are is always in view. #} -
    + {% endfor %} @@ -95,7 +96,7 @@ - +
    @@ -116,7 +117,7 @@ @@ -141,7 +142,7 @@ {% for x in film %} - {%- if x.kind == 'image' %}{% else %}{{ '♪' if x.kind == 'audio' else '▶' }}{% endif -%} + {{ x.name }}{%- if x.kind == 'image' %}{% else %}{{ '♪' if x.kind == 'audio' else '▶' }}{% endif -%} {{ num(x.ordinal) }} {% endfor %} diff --git a/docs/contracts/as_antislop.contract.md b/docs/contracts/as_antislop.contract.md index d654e9b..6e013c2 100644 --- a/docs/contracts/as_antislop.contract.md +++ b/docs/contracts/as_antislop.contract.md @@ -14,6 +14,8 @@ touches: - "booth/templates/_marks.html, _ask_inline.html, booth.html (S1)" - "booth/templates/base.html, view.html; booth/static/embed.js (S2)" - "booth/templates/base.html, view.html, compare.html; tests/test_antislop_browser.py (S3)" + - "booth/templates/base.html (S4, S6)" + - "booth/app.py (`human_dur`); every page template; booth/static/embed.js (S5a)" - "tests/test_antislop.py; tests/mutations/antislop.toml" assumptions: - "ONE VIEWER, on this box: local time is the operator's time (US Pacific), as the existing date filters already assume." @@ -129,6 +131,40 @@ The run found that the Booth is sound on desktop and has a set of problems a vie - **The hazard stripe sits on a `::before`,** not on the button's background, for Wipe now and the armed bulk delete. The button's own background is honestly transparent (a detector read the 3px background band as the whole background, 1.0:1), and the stripe renders exactly as before. - *Falsifiable:* `test_the_hazard_stripe_is_a_pseudo_element`, in a real browser: no gradient on the button, a 3px striped `::before`. -## S5 +## S5a — names, landmarks, focus rings, hit areas -Interaction and screen readers: focus after a swap, a status slot the viewer cannot cover, live-region timing, names, keys, focus rings, hit areas, and the unsaved-draft guard. It rewires the in-place client, so it gets its own contract section and review before any code. +The markup and CSS half of the interaction work. It changes no script behaviour except where Wipe now's prompt comes from. The in-place client is untouched; that half is S5b. + +- **Every link and button has a word for a name.** A control a screen reader would announce as "×", "⬇", "⤢", "☆", "1:1" or "01" carries an `aria-label`, or the file's name as `.sr-only` text. The visible label stays inside the name (`1:1, natural pixels`). + - Covered: the withdraw ×s, downloads, open-full-page, the viewers' close ✕, the board's pin, copy and remove, the bench's remove, the zoom toggle, and the film-strip and flagged-tray frames. + - A Desk row's wipe names its booth (`wipe the booth alpha`), so a list of rows is not a list of identical "wipe booth"s. + - *Falsifiable:* `test_every_control_has_a_word_for_a_name` (every page, the link board included; the name is computed from `aria-label`, else the text plus each image's `alt`), and `test_desk_row_controls_name_their_booth`. +- **Every field has a name that is not its placeholder.** Every note field, the bench's two inputs and the inline ask's notes carry an `aria-label`. + - *Falsifiable:* `test_fields_are_named`, on every page and on both embed placements. +- **An ask's options are a named group, and its ids are unique.** The inline ask's options are `role="radiogroup"`, labelled by the question's prompt. The marks page's single-question fieldset gets a visually hidden ``. A titled ask's title takes `bk-ask--title`: it used to reuse `bk-ask-`, which the question already holds. + - *Falsifiable:* `test_radio_groups_are_named_and_ids_are_unique`, checked on each placement the embed makes: either `whole`, or the questions plus `submit`. +- **Every page has one h1, a skip link and a named main.** The Desk, the review and compare get a visually hidden h1. A doc's own h1 is content and is not counted. + - *Falsifiable:* `test_every_page_has_one_h1_and_a_skip_link`. +- **The browser chrome matches the theme:** `theme-color` for light (`#f0f4f5`) and for dark (`#15191d`). + - *Falsifiable:* `test_theme_color_for_both_schemes`. +- **The review's progress tape is one picture** (`role="img"`, named "N of M seen"). Its segments leave the tab order: the film strip below it holds the same links, named. + - *Falsifiable:* `test_the_tape_is_one_picture`. +- **Wipe now asks by name.** The Desk's delegated prompt moves to `base.html`, and a booth page's Wipe now uses it. It names the booth, and asks the kept-booth question for a kept booth. This replaces an inline `confirm('Wipe this booth now?')`. With JS off the form still submits, as before. + - *Falsifiable:* `test_wipe_now_asks_by_name` (markup), and `test_wipe_now_asks_by_name_in_the_browser`: the dialog's text, and dismissing it wipes nothing. +- **Focus rings and hit areas.** + - The embed draws its own focus rings, so a host's `outline:none` cannot remove them. + - Rings inside `overflow:hidden` containers are drawn inside (`outline-offset:-2px`), where they cannot be clipped. + - The withdraw × is at least 24px, and 44px under a coarse pointer. + - Controls take `touch-action:manipulation`, and the scrolling strips contain their overscroll. + - A long booth slug wraps on a phone. + - *Falsifiable:* `test_embed_chrome_draws_its_own_focus_rings`, `test_focus_rings_are_drawn_inside_clipping_containers`, `test_withdraw_buttons_are_big_enough_to_hit` (measured at 1280, and at 390 with touch), and `test_touch_and_scroll_behaviour` (computed style). +- **Small truths.** + - A why truncated with an ellipsis carries its full text in `title`. + - A countdown of 48h or more rolls up to days (`6d 23h`, not `167h 12m`). + - *Falsifiable:* `test_a_truncated_why_carries_its_full_text`, `test_human_dur_rolls_up_to_days`. +- **Existing rows this slice edits** (booth-dev's): two `r2_flow.toml` rows for the confirm helper now name `base.html`, where the helper moved. Their anchors are unchanged. +- **Reported, not changed:** mark ids repeat across a tile and its aside (`mark-note-1`). The CLI prints `#mark-` links to them, so the fix is booth-dev's call. + +## S5b + +The in-place client half: focus restored after a swap, a status slot the viewer cannot cover (and `inert` behind it), live-region timing and "Saving…", key handling, and the unsaved-draft guard. It rewires the in-place client, so it gets its own contract section and review before any code. diff --git a/tests/mutations/antislop.toml b/tests/mutations/antislop.toml index e557614..ecead2d 100644 --- a/tests/mutations/antislop.toml +++ b/tests/mutations/antislop.toml @@ -375,3 +375,271 @@ file = "booth/templates/base.html" test = "tests/test_antislop_browser.py::test_the_hazard_stripe_is_a_pseudo_element" old = """ .wipe-lg button::before,.board-del-sel:not(:disabled)::before{content:"";position:absolute;""" new = """ .wipe-lg button::after,.board-del-sel:not(:disabled)::after{content:"";position:absolute;""" + +# ---- S5a: accessibility plumbing (names, landmarks, focus, hit areas) + +[[mutation]] +label = "S5a the withdraw-note × unnamed again" +file = "booth/templates/_marks.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """""" +new = """""" + +[[mutation]] +label = "S5a a film frame named only by its number" +file = "booth/templates/view.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """ {{ x.name }}{%- if x.kind == 'image' %}{% else %}""" +new = """ {%- if x.kind == 'image' %}{% else %}""" + +[[mutation]] +label = "S5a a flagged-tray item named only by its number" +file = "booth/templates/_marks.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """{{ it.name }}{%- if it.kind""" +new = """{%- if it.kind""" + +[[mutation]] +label = "S5a the 1:1 button read as digits" +file = "booth/templates/view.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """ aria-label="1:1, natural pixels" title="natural pixels — drag to pan a large picture">""" +new = """ title="natural pixels — drag to pan a large picture">""" + +[[mutation]] +label = "S5a the doc's open-full-page glyph unnamed" +file = "booth/templates/booth.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """ title="open full page" aria-label="open {{ it.name }} full page">⤢""" +new = """ title="open full page">⤢""" + +[[mutation]] +label = "S5a compare's close glyph unnamed" +file = "booth/templates/compare.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """ title="back to the review of A (Esc)" aria-label="back to the review of A">✕""" +new = """ title="back to the review of A (Esc)">✕""" + +[[mutation]] +label = "S5a the board's pin star unnamed" +file = "booth/templates/booth.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """ aria-pressed="{{ 'true' if e.pinned else 'false' }}" aria-label="pin {{ e.desc }}"""" +new = """ aria-pressed="{{ 'true' if e.pinned else 'false' }}"""" + +[[mutation]] +label = "S5a the board's remove × unnamed" +file = "booth/templates/booth.html" +test = "tests/test_antislop.py::test_every_control_has_a_word_for_a_name" +old = """ title="remove this link" aria-label="remove the link {{ e.desc }}"""" +new = """ title="remove this link"""" + +[[mutation]] +label = "S5a a Desk wipe that does not say which booth" +file = "booth/templates/index.html" +test = "tests/test_antislop.py::test_desk_row_controls_name_their_booth" +old = """aria-label="wipe the booth {{ b.name }}"""" +new = """aria-label="wipe booth"""" + +[[mutation]] +label = "S5a the note field named only by its placeholder" +file = "booth/templates/_marks.html" +test = "tests/test_antislop.py::test_fields_are_named" +old = """