The last of the anti-slop interaction work (guidelines G6, G7, G14, G15,
G17), plus booth-dev's note from S5b's gate. Every S5b promise holds: no
re-POST, serialized saves, a batch never reloads, focus survives a swap.
- Keys (G6): one rule in base.html's <head>, BoothKeys.theirs(e), called
first by the grid, the review and compare. A field or a player owns every
key but Escape (Esc still goes back from a focused player); a control
owns Space; a focused 1:1 stage that can pan owns the
arrows and Space (Chromium puts it in the Tab order); Ctrl/Meta/Alt are
the browser's. The field check lives on as BoothKeys.isEditable. Before:
an arrow on a focused video left the review, and Enter on any control
also opened the grid cursor's tile.
- The grid cursor is real focus: the tile it moves to gets tabindex=-1
(script-set, one tile at a time) and focus, without a scroll; the cursor
is an item (its data-item), and a doc closed with its ✕ is skipped; focus that
lands on a tile (S5b's fallback) makes it the cursor; Enter opens the
review only from the body, the grid or the tile, by its view?f= link;
n opens a closed doc's fold; Escape clears the cursor
and releases the tile's focus. The reticle is its focus mark (no second
ring).
- The doc bar (G7): the controls leave the <summary>. div.doc-bar holds
details.doc-fold (its summary is the label only) and div.doc-tools beside
it; the body and notes follow in div.doc-inline, hidden with a closed
fold by :has(), scripts on or off. A closed doc keeps its tools. Renders
pixel-identical to today at 1280 and 390, light and dark.
- Tile sizes (G14): a gallery tile's <img> carries width/height, the
picture as the browser draws it (EXIF 5-8 swap), read from the header
only (no decode; PNG getexif is skipped unless the header carried it),
opened O_NOFOLLOW|O_NONBLOCK, cached by the file's identity (ctime
included, so cp -p over a file is seen), in a separate
step (items.image_dims over thumbs.drawn_size) so the Desk never pays it.
Measured before: a link to tile 30 of 40 landed 44px low (3/3); after, on
its mark. content-visibility:auto, which the report proposed too, is NOT
added: a swapped-in tile has no remembered size, and a flag far down moved
the page 2929px (3/3; 0px without it).
- The rail (G15): html:has(.rail){scroll-padding-top} replaces .item's
scroll-margin-top (the two add), so a control reached by Tab stops below
the sticky rail too. Measured before: a Tab-focused flag button at 19.6px,
under the rail's bottom at 47.6px. The scripts-off fallbacks are the old
rules' numbers (132px, 217px at <=480), now pinned by a test. The height
script follows the live rail after every in-place save (it watched the
replaced node, and read 0px after one flag), and the rail's own controls
cancel the padding (a Tab between stuck group links scrolled 357px).
- Reveal names (G17): no aria-label on any reveal control; the name is the
words on it, the glyph in an aria-hidden span, the item's name as
.sr-only text ("reveal a.png" / "hide a.png"). Reveal all drops
aria-pressed (its words already say the state; r2b rules them) and its
"on" look reads the .reveal-all class on <html>. No pixel changes.
- booth-dev's note: a refused batch's forms enter `unsent` with the
refusal's words, and a later save says every standing failure's words
(each once, in order) instead of "Saved.", and every warning says the
other standing failures first, so no failure buries another. Test first:
test_a_batch_refusal_outlives_an_unrelated_save.
- Rows re-anchored to the same failure: r2b "Space on a focused review
button", r3 "C3 a held modifier" and both "C3 Space on a focused ..."
(now in BoothKeys), r2c "the stage reveal shows with scripts off", and
this contract's S3 doc-bar row and five S5b status-line rows.
Folded from the heid contract review (BEINKA, panel 4/4, thread
01M3NZJNX8D3BEYD48M9K3MV3Q): 24 flags, all prose the tests left open; the
contract states the tile/focus/cursor seam with S5b, the helper's union and
scope, the size's source and every path to none, Reveal all's name, the
refusal sentence's lifetime, and the fallback arithmetic (one test added).
Folded from the heid bug-hunt (HRÖSKVA, panel 4/4, thread
01M3P0ZPRSASFSE5K3PR4NTQP6): R1 closed docs and the cursor as an item, R2
the rail's height after a save, R3 no warning buries another, R5 the view?f=
link, R7 ctime in the size cache, R8 Escape from a player, R9 the rail's own
controls, R10 n on a closed doc. Refuted with reasons: R4 (unreachable: refused
picks re-send together), R6 (Chrome takes the same header's size with or
without the attributes; measured), R11 (by design).
From this slice's own falsifier runs: a "one row wide" row that mutated a
flex basis a non-wrapping bar just shrinks (re-aimed at the bar's flex), and
a Reveal-all "on look" read under the clicking pointer, where :hover draws
the same border (the pointer now leaves first; 3/3 proved).
Contract: as_antislop S5c.
Falsifiers: antislop.toml S5c section.
569 lines
23 KiB
TOML
569 lines
23 KiB
TOML
# R2b — the Desk row, Reveal all, the theme toggle: every falsifier the
|
|
# contract claims (docs/contracts/r2b_desk_reveal_theme.contract.md), and the
|
|
# change each forbids. Merge 1 is D2 + D2b (the blur half); merge 2 adds D1 + D3.
|
|
|
|
unit = "reveal all, the booth blur toggle (merge 1)"
|
|
|
|
[[mutation]]
|
|
label = "D2 reveal all does not lift the tile's blur"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_reveals_every_blurred_surface_and_survives_the_next_page"
|
|
old = '''
|
|
.reveal-all .item.blurred img,.reveal-all .item.blurred video,'''
|
|
new = '''
|
|
.reveal-all-OFF .item.blurred img,.reveal-all .item.blurred video,'''
|
|
|
|
[[mutation]]
|
|
label = "D2 no pre-paint re-application: the next page of the reel is blurred again"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_reveals_every_blurred_surface_and_survives_the_next_page"
|
|
old = '''
|
|
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');'''
|
|
new = '''
|
|
if (false) d.classList.add('reveal-all');'''
|
|
|
|
[[mutation]]
|
|
label = "D2 the reveal is not scoped to the booth (any reveal in the tab reveals every booth)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_on_booth_a_does_not_reveal_booth_b_or_the_desk"
|
|
old = '''
|
|
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');'''
|
|
new = '''
|
|
if (b !== null && sessionStorage.length > 0) d.classList.add('reveal-all');'''
|
|
|
|
[[mutation]]
|
|
label = "D2 per-tile reveal buttons do not stand down under reveal all"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_survives_an_in_place_save"
|
|
old = '''
|
|
.reveal-all .item.blurred .reveal,.reveal-all #vreveal{display:none}'''
|
|
new = '''
|
|
.reveal-all-OFF .item.blurred .reveal,.reveal-all #vreveal{display:none}'''
|
|
|
|
[[mutation]]
|
|
label = "D2 blur again wipes each item's own reveal"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_blur_again_restores_each_items_own_reveal"
|
|
old = '''
|
|
var on = d.classList.toggle('reveal-all');
|
|
try {'''
|
|
new = '''
|
|
var on = d.classList.toggle('reveal-all');
|
|
document.querySelectorAll('.item.revealed').forEach(function (i) { i.classList.remove('revealed'); });
|
|
try {'''
|
|
|
|
[[mutation]]
|
|
label = "D2 a storage write that throws swallows the click"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click"
|
|
old = '''
|
|
var on = d.classList.toggle('reveal-all');
|
|
try {'''
|
|
new = '''
|
|
sessionStorage.setItem(key, '1');
|
|
var on = d.classList.toggle('reveal-all');
|
|
try {'''
|
|
|
|
[[mutation]]
|
|
label = "D2 the control shows without JS (server markup not hidden)"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click"
|
|
old = '''
|
|
{% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden title='''
|
|
new = '''
|
|
{% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all title='''
|
|
|
|
[[mutation]]
|
|
label = "D2 the control is markup even when nothing is blurred"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_flow.py::test_reveal_all_is_in_the_markup_only_when_something_is_blurred_and_always_hidden"
|
|
old = '''
|
|
{% if all_items | selectattr('blurred') | list %}<button'''
|
|
new = '''
|
|
{% if true %}<button'''
|
|
|
|
[[mutation]]
|
|
label = "D2 the review page does not carry data-booth"
|
|
file = "booth/templates/view.html"
|
|
test = "tests/test_flow.py::test_reveal_all_is_in_the_markup_only_when_something_is_blurred_and_always_hidden"
|
|
old = '''
|
|
{% block html_attrs %} data-booth="{{ name }}"{% endblock %}'''
|
|
new = '''
|
|
{% block html_attrs %}{% endblock %}'''
|
|
|
|
[[mutation]]
|
|
label = "D2b the header control's label does not follow the server's fog state"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
|
|
old = '''
|
|
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
|
|
<button title="{{ 'un-blur the whole booth'''
|
|
new = '''
|
|
<input type="hidden" name="on" value="1">
|
|
<button title="{{ 'un-blur the whole booth'''
|
|
|
|
[[mutation]]
|
|
label = "D2b the review's control drops `back` (fogging ejects you from the review)"
|
|
file = "booth/templates/view.html"
|
|
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
|
|
old = '''
|
|
<input type="hidden" name="back" value="{{ file }}">'''
|
|
new = '''
|
|
'''
|
|
|
|
[[mutation]]
|
|
label = "D2b the Desk row does not say a booth is fogged"
|
|
file = "booth/templates/index.html"
|
|
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
|
|
old = '''
|
|
{% if b.booth_blurred %}<span class="badge badge-blur"'''
|
|
new = '''
|
|
{% if false %}<span class="badge badge-blur"'''
|
|
|
|
[[mutation]]
|
|
label = "D2b an item blurred only by the booth offers a per-item un-blur that does nothing"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_flow.py::test_under_a_fogged_booth_each_items_blur_control_tells_the_truth"
|
|
old = '''
|
|
{% if it.blurred and not it.blurred_self %}'''
|
|
new = '''
|
|
{% if false %}'''
|
|
|
|
[[mutation]]
|
|
label = "D2b the per-item control reads the composed blur, not the item's own"
|
|
file = "booth/app.py"
|
|
test = "tests/test_flow.py::test_under_a_fogged_booth_each_items_blur_control_tells_the_truth"
|
|
old = '''
|
|
"blurred_self": it.blurred_self,'''
|
|
new = '''
|
|
"blurred_self": it.blurred,'''
|
|
|
|
# ---- folds: the heid code-review ("BLITZ-2") and bug-hunt ("FENRIR-6") panels on merge 1
|
|
|
|
[[mutation]]
|
|
label = "a board holding files loses the blur controls its labels point at"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_flow.py::test_a_board_with_files_gets_the_blur_controls_its_labels_point_at"
|
|
old = '''
|
|
{% if all_items %}
|
|
{# The fog form IS a region'''
|
|
new = '''
|
|
{% if not board %}
|
|
{# The fog form IS a region'''
|
|
|
|
[[mutation]]
|
|
label = "a blurred doc's own page renders clear"
|
|
file = "booth/templates/doc.html"
|
|
test = "tests/test_flow.py::test_a_blurred_docs_own_page_is_blurred_too"
|
|
old = '''
|
|
<div class="docbody{% if blurred %} is-blurred{% endif %}" id="docbody">'''
|
|
new = '''
|
|
<div class="docbody" id="docbody">'''
|
|
|
|
[[mutation]]
|
|
label = "the review offers Reveal all when only a doc (off the ring) is blurred"
|
|
file = "booth/templates/view.html"
|
|
test = "tests/test_flow.py::test_reveal_all_renders_where_it_can_act"
|
|
old = '''
|
|
{% if film | selectattr('blurred') | list %}<button'''
|
|
new = '''
|
|
{% if true %}<button'''
|
|
|
|
[[mutation]]
|
|
label = "the fog form is a GET (changes nothing with scripts off)"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
|
|
old = '''<form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="post" action="/b/{{ name_url }}/blurbooth">
|
|
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
|
|
<button title="{{ 'un-blur the whole booth — per-item'''
|
|
new = '''<form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="get" action="/b/{{ name_url }}/blurbooth">
|
|
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
|
|
<button title="{{ 'un-blur the whole booth — per-item'''
|
|
|
|
[[mutation]]
|
|
label = "the swap stops carrying an item's own reveal"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_an_items_own_reveal_survives_an_in_place_save"
|
|
old = '''
|
|
['revealed', 'is-closed'].forEach(function (c) {'''
|
|
new = '''
|
|
['is-closed'].forEach(function (c) {'''
|
|
|
|
[[mutation]]
|
|
label = "a storage READ that throws raises out of the pre-paint script"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click"
|
|
old = '''
|
|
try {
|
|
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');
|
|
} catch (e) {}'''
|
|
new = '''
|
|
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');'''
|
|
|
|
[[mutation]]
|
|
label = "fogging writes through a planted marker link"
|
|
file = "booth/app.py"
|
|
test = "tests/test_flow.py::test_fogging_never_writes_through_a_planted_marker_link"
|
|
old = '''
|
|
try:
|
|
os.lstat(marker)
|
|
return True
|
|
except FileNotFoundError:
|
|
pass
|
|
try:
|
|
os.close(os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o644))
|
|
except FileExistsError:
|
|
pass # lost a race to another fog: still fogged
|
|
return True'''
|
|
new = '''
|
|
marker.touch(exist_ok=True)
|
|
return True'''
|
|
|
|
[[mutation]]
|
|
label = "the fog landing echoes `back` unchecked"
|
|
file = "booth/app.py"
|
|
test = "tests/test_flow.py::test_the_fog_landing_is_built_from_the_ring_never_echoed"
|
|
old = '''
|
|
if back and back in review_chain(booth_items(booth)):'''
|
|
new = '''
|
|
if back:'''
|
|
|
|
[[mutation]]
|
|
label = "the fog form is outside every region (a swap leaves its label stale)"
|
|
file = "booth/templates/booth.html"
|
|
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
|
|
old = '''
|
|
<span class="region-wrap" data-region="blur-booth"><form class="blur-all'''
|
|
new = '''
|
|
<span class="region-wrap"><form class="blur-all'''
|
|
|
|
[[mutation]]
|
|
label = "Space on a focused review button moves to the next item"
|
|
# design-dev re-anchored this row in S5c: the review's own Space exception moved
|
|
# into base.html's BoothKeys, the one rule the grid, the review and compare
|
|
# share. Same failure: Space from a focused control reaches the page.
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_space_on_a_focused_review_button_presses_it_and_does_not_move_on"
|
|
old = ''' if (k === ' ' && t.closest('button, a[href], summary')) return true;
|
|
'''
|
|
new = ''''''
|
|
|
|
[[mutation]]
|
|
label = "the top-bar controls squeeze into multi-line stacks at phone width"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_review_and_doc_top_bars_fit_a_phone"
|
|
# r3 re-anchored this row. r3 added a Compare control to the review's bar and
|
|
# made a full bar WRAP at phone width (a fogged booth already overflowed it by
|
|
# 3px at 390). A wrapping bar never squeezes, so removing the no-wrap rules
|
|
# alone went VACUOUS: the property is now held by both. The defeating change is
|
|
# losing both; r3.toml rows the wrap on its own.
|
|
old = '''
|
|
.blur-all button,.reveal-all-btn{white-space:nowrap}
|
|
@media (max-width:600px){.reveal-all-btn .ra-note{display:none}}
|
|
/* R3: the review's Compare control keeps only its glyph at phone width; its
|
|
title still says what it does (and C does it). */
|
|
@media (max-width:600px){.vcompare-l{display:none}}
|
|
/* ...and at phone width a top bar that cannot hold its controls WRAPS rather
|
|
than scrolling the page sideways. The review's bar was full before R3 (a
|
|
fogged booth overflowed it by 3px at 390); compare's bar holds more. */
|
|
@media (max-width:600px){.vbar{flex-wrap:wrap;row-gap:6px}}'''
|
|
new = '''
|
|
@media (max-width:600px){.vcompare-l{display:none}}'''
|
|
|
|
[[mutation]]
|
|
label = "the Desk strip under another booth's reveal is lifted by a whisker (blur(0px) is not blurred)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_on_booth_a_does_not_reveal_booth_b_or_the_desk"
|
|
old = '''
|
|
.blurred-thumb{filter:blur(16px)}'''
|
|
new = '''
|
|
.blurred-thumb{filter:blur(0px)}'''
|
|
|
|
# ---- the flake: the test browser has no internet (positive control per file)
|
|
|
|
[[mutation]]
|
|
label = "the flow test browser can reach the internet (Google Fonts can stall networkidle)"
|
|
file = "tests/test_flow_browser.py"
|
|
test = "tests/test_flow_browser.py::test_the_test_browser_has_no_internet"
|
|
old = '''
|
|
b = pw.chromium.launch(args=OFFLINE)'''
|
|
new = '''
|
|
b = pw.chromium.launch()'''
|
|
|
|
[[mutation]]
|
|
label = "the embed test browser can reach the internet"
|
|
file = "tests/test_embed_browser.py"
|
|
test = "tests/test_embed_browser.py::test_the_test_browser_has_no_internet"
|
|
old = '''
|
|
b = pw.chromium.launch(args=OFFLINE)'''
|
|
new = '''
|
|
b = pw.chromium.launch()'''
|
|
|
|
# ---- merge 2: D1 the Desk row, D1b dates, D3 the theme toggle
|
|
# Successors to the four r2_flow rows retired for D1. One branch is proved by
|
|
# reading, not here: a true touch LAPTOP (fine pointer + a coarse one) cannot be
|
|
# emulated — Chromium's touch emulation makes the primary pointer coarse.
|
|
|
|
[[mutation]]
|
|
label = "D1 the cluster is visible at rest"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_row_controls_take_no_room_where_a_hover_exists"
|
|
old = '''
|
|
opacity:0;pointer-events:none;transition:opacity var(--dur-1) var(--ease-out)}'''
|
|
new = '''
|
|
opacity:1;pointer-events:none;transition:opacity var(--dur-1) var(--ease-out)}'''
|
|
|
|
[[mutation]]
|
|
label = "D1 hover shows the cluster but leaves it unclickable"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_row_controls_take_no_room_where_a_hover_exists"
|
|
old = '''
|
|
.desk-row:hover .desk-acts,.desk-row:focus-within .desk-acts{opacity:1;pointer-events:auto}'''
|
|
new = '''
|
|
.desk-row:hover .desk-acts,.desk-row:focus-within .desk-acts{opacity:1}'''
|
|
|
|
[[mutation]]
|
|
label = "D1 the cluster stays in flow where a hover exists (it takes room)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_row_controls_take_no_room_where_a_hover_exists"
|
|
old = '''
|
|
.desk-acts{position:absolute;top:17px;left:calc(12px + 210px - 5px);transform:translateX(-100%);flex:none;gap:4px;padding:3px;'''
|
|
new = '''
|
|
.desk-acts{position:static;top:17px;left:calc(12px + 210px - 5px);transform:translateX(-100%);flex:none;gap:4px;padding:3px;'''
|
|
|
|
[[mutation]]
|
|
label = "D1 hover-only everywhere (no controls at all on touch)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_on_touch_the_row_controls_are_visible_in_flow_and_at_least_28px"
|
|
old = '''
|
|
.desk-acts{flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px}'''
|
|
new = '''
|
|
.desk-acts{flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px;opacity:0}'''
|
|
|
|
[[mutation]]
|
|
label = "D1 touch controls fall below the 28px floor"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_on_touch_the_row_controls_are_visible_in_flow_and_at_least_28px"
|
|
old = '''
|
|
min-height:32px;min-width:32px;padding:0 10px;font-family:var(--font-mono);font-size:var(--size-caption);'''
|
|
new = '''
|
|
min-height:18px;min-width:18px;padding:0 10px;font-family:var(--font-mono);font-size:var(--size-caption);'''
|
|
|
|
[[mutation]]
|
|
label = "D1 zip back in the middle"
|
|
file = "booth/templates/index.html"
|
|
test = "tests/test_flow_browser.py::test_the_row_controls_run_zip_keep_or_release_then_wipe"
|
|
old = '''
|
|
<a class="dl-link" href="/b/{{ b.name_url }}/?download=1" title="download this booth as a zip">⬇ zip</a>
|
|
{%- if b.kept %}
|
|
<form class="release"'''
|
|
new = '''
|
|
{%- if b.kept %}
|
|
<a class="dl-link" href="/b/{{ b.name_url }}/?download=1" title="download this booth as a zip">⬇ zip</a>
|
|
<form class="release"'''
|
|
|
|
[[mutation]]
|
|
label = "D1 a held booth's pill reads as counting down"
|
|
file = "booth/templates/index.html"
|
|
test = "tests/test_flow.py::test_the_lifetime_pill_class_is_kept_held_or_counting"
|
|
old = '''('life-held' if b.hold in ('open', 'unreadable') else 'life-count')'''
|
|
new = '''('life-held' if b.hold == 'never' else 'life-count')'''
|
|
|
|
[[mutation]]
|
|
label = "D1b an unknown birth time renders a guess"
|
|
file = "booth/templates/_dates.html"
|
|
test = "tests/test_flow.py::test_created_and_updated_are_dated_facts_and_none_says_nothing"
|
|
old = '''{% macro dates(created_at, landed_at, now) -%}'''
|
|
new = '''{% macro dates(created_at, landed_at, now) -%}{%- set created_at = created_at or landed_at -%}'''
|
|
|
|
[[mutation]]
|
|
label = "D1b updated is measured from the wrong clock"
|
|
file = "booth/templates/_dates.html"
|
|
test = "tests/test_flow.py::test_created_and_updated_are_dated_facts_and_none_says_nothing"
|
|
old = '''updated {{ age|ago }}'''
|
|
new = '''updated {{ (now - created_at)|ago }}'''
|
|
|
|
[[mutation]]
|
|
label = "D3 a stored theme is not applied at load (a reload forgets it)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_theme_toggle_forces_light_and_dark_and_system_follows_the_os_live"
|
|
old = '''
|
|
var t = localStorage.getItem('booth.theme');
|
|
if (t === 'light' || t === 'dark') d.setAttribute('data-theme', t);'''
|
|
new = '''
|
|
var t = localStorage.getItem('booth.theme');
|
|
if (false) d.setAttribute('data-theme', t);'''
|
|
|
|
[[mutation]]
|
|
label = "D3 System snapshots the OS instead of following it live"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_theme_toggle_forces_light_and_dark_and_system_follows_the_os_live"
|
|
old = '''
|
|
if (c === 'system') d.removeAttribute('data-theme'); else d.setAttribute('data-theme', c);'''
|
|
new = '''
|
|
if (c === 'system') d.setAttribute('data-theme', matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'); else d.setAttribute('data-theme', c);'''
|
|
|
|
[[mutation]]
|
|
label = "D3 forced light is not in the sheet"
|
|
file = "booth/templates/_svos_tokens.css"
|
|
test = "tests/test_flow_browser.py::test_the_theme_toggle_forces_light_and_dark_and_system_follows_the_os_live"
|
|
old = '''
|
|
/* ... or the viewer forced light. Same declarations as above, by construction. */
|
|
:root[data-theme="light"] {'''
|
|
new = '''
|
|
/* ... or the viewer forced light. Same declarations as above, by construction. */
|
|
:root[data-theme="light-OFF"] {'''
|
|
|
|
[[mutation]]
|
|
label = "D3 the toggle shows without JS (display beats [hidden])"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_theme_toggle_never_shows_without_js_and_a_storage_failure_still_applies"
|
|
old = '''
|
|
.theme[hidden]{display:none}'''
|
|
new = '''
|
|
'''
|
|
|
|
[[mutation]]
|
|
label = "D3 a storage write that throws swallows the choice"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_theme_toggle_never_shows_without_js_and_a_storage_failure_still_applies"
|
|
old = '''
|
|
var c = b.getAttribute('data-theme-choice');
|
|
if (c === 'system')'''
|
|
new = '''
|
|
var c = b.getAttribute('data-theme-choice');
|
|
localStorage.setItem('booth.theme', c);
|
|
if (c === 'system')'''
|
|
|
|
[[mutation]]
|
|
label = "D3 a stylesheet precedes the stored-theme script (a flash)"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow.py::test_a_forced_theme_is_applied_before_first_paint"
|
|
old = '''
|
|
<script>
|
|
(function () {
|
|
var d = document.documentElement, b = d.getAttribute('data-booth');'''
|
|
new = '''
|
|
<style></style>
|
|
<script>
|
|
(function () {
|
|
var d = document.documentElement, b = d.getAttribute('data-booth');'''
|
|
|
|
[[mutation]]
|
|
label = "D3 the ask chrome in a verbatim page ignores the choice"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_flow_browser.py::test_a_forced_theme_reaches_the_ask_chrome_inside_a_verbatim_page"
|
|
old = '''
|
|
bkTheme();
|
|
reassociate();'''
|
|
new = '''
|
|
reassociate();'''
|
|
|
|
[[mutation]]
|
|
label = "D3 the ask chrome does not follow a choice made in another tab"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_flow_browser.py::test_a_forced_theme_reaches_the_ask_chrome_inside_a_verbatim_page"
|
|
old = '''
|
|
if (e.key === "booth.theme" || e.key === null) bkTheme();'''
|
|
new = '''
|
|
if (false) bkTheme();'''
|
|
|
|
# ---- merge-2 folds: heid bug-hunt ("GORE-7") and code-review ("STAGGER-3")
|
|
# Not expressible as one replacement, so pinned by their tests directly: the
|
|
# row's DOM tab order (read in order by the test), and a declaration dropped
|
|
# from BOTH light copies (the expected set is now written from SVOS, not
|
|
# derived from the copies).
|
|
|
|
[[mutation]]
|
|
label = "a date the calendar cannot hold raises through the Desk"
|
|
file = "booth/app.py"
|
|
test = "tests/test_flow.py::test_a_date_no_calendar_can_hold_renders_nothing_and_never_500s"
|
|
old = '''
|
|
_BAD_DATE = (OverflowError, OSError, ValueError)'''
|
|
new = '''
|
|
_BAD_DATE = ()'''
|
|
|
|
[[mutation]]
|
|
label = "updated is dropped for content older than its booth (one-sided gap)"
|
|
file = "booth/templates/_dates.html"
|
|
test = "tests/test_flow.py::test_updated_shows_whenever_it_differs_from_created_and_a_future_one_says_its_date"
|
|
old = '''(landed_at - created_at)|abs >= 60'''
|
|
new = '''(landed_at - created_at) >= 60'''
|
|
|
|
[[mutation]]
|
|
label = "a content clock ahead of now reads as an age"
|
|
file = "booth/templates/_dates.html"
|
|
test = "tests/test_flow.py::test_updated_shows_whenever_it_differs_from_created_and_a_future_one_says_its_date"
|
|
old = '''{%- if age < -60 %}'''
|
|
new = '''{%- if false %}'''
|
|
|
|
[[mutation]]
|
|
label = "hours run on to 47h (1d ago never appears)"
|
|
file = "booth/app.py"
|
|
test = "tests/test_flow.py::test_an_age_is_said_in_its_largest_whole_unit"
|
|
old = '''
|
|
if s < 86400:
|
|
return f"{s // 3600}h ago"'''
|
|
new = '''
|
|
if s < 2 * 86400:
|
|
return f"{s // 3600}h ago"'''
|
|
|
|
[[mutation]]
|
|
label = "a theme chosen in one tab does not reach the Booth's other open tabs"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_a_theme_chosen_in_one_tab_moves_the_others"
|
|
old = '''
|
|
if (e.key !== 'booth.theme' && e.key !== null) return;
|
|
var t = null;'''
|
|
new = '''
|
|
return;
|
|
var t = null;'''
|
|
|
|
[[mutation]]
|
|
label = "the theme mark reaches the author's own .bk-ask"
|
|
file = "booth/static/embed.js"
|
|
test = "tests/test_flow_browser.py::test_the_theme_marks_only_the_ask_fragments_we_mounted"
|
|
old = '''
|
|
ours.forEach(function (root) {'''
|
|
new = '''
|
|
[document.body].forEach(function (root) {'''
|
|
|
|
[[mutation]]
|
|
label = "keyboard focus no longer reveals the row's controls"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_pill_shows_at_rest_and_focus_reveals_the_controls"
|
|
old = '''
|
|
.desk-row:hover .desk-acts,.desk-row:focus-within .desk-acts{opacity:1;pointer-events:auto}'''
|
|
new = '''
|
|
.desk-row:hover .desk-acts{opacity:1;pointer-events:auto}'''
|
|
|
|
[[mutation]]
|
|
label = "the lifetime pill hides at rest"
|
|
file = "booth/templates/base.html"
|
|
test = "tests/test_flow_browser.py::test_the_pill_shows_at_rest_and_focus_reveals_the_controls"
|
|
old = '''
|
|
.life-count::before{content:"◷"}'''
|
|
new = '''
|
|
.life-count::before{content:"◷"}
|
|
.life{opacity:0}'''
|
|
|
|
[[mutation]]
|
|
label = "high contrast never applies (dark-hc block gone)"
|
|
file = "booth/templates/_svos_tokens.css"
|
|
test = "tests/test_flow_browser.py::test_a_forced_theme_follows_high_contrast"
|
|
old = '''
|
|
/* dark high contrast: whenever dark is in effect (light, below, outranks it) */
|
|
@media (prefers-contrast: more) {'''
|
|
new = '''
|
|
/* dark high contrast: whenever dark is in effect (light, below, outranks it) */
|
|
@media (prefers-contrast: less) {'''
|
|
|
|
[[mutation]]
|
|
label = "Reveal all does not reach a blurred doc's own page"
|
|
file = "booth/templates/doc.html"
|
|
test = "tests/test_flow_browser.py::test_reveal_all_lifts_the_doc_page_it_reaches"
|
|
old = '''
|
|
.reveal-all .docbody.is-blurred .markdown-body,.reveal-all .docbody.is-blurred .textview{filter:none}'''
|
|
new = '''
|
|
.reveal-all-OFF .docbody.is-blurred .markdown-body,.reveal-all .docbody.is-blurred .textview{filter:none}'''
|