fix(desk): the heid code-review and bug-hunt panels on r2b merge 2, folded

The bug hunt (4/4) and code review (4/4) were both clean on mechanism.
Their shared catch was the one-sided minute check.

Dates:
- The date filters never raise. One clock outside the calendar's range
  500'd the Desk for every booth, because every row renders in one
  response. An unrenderable date now renders nothing.
- "Updated" shows whenever it differs from "created" by a minute or more,
  either way. Copied content is often older than its folder.
- A clock ahead of now shows its date, never "just now".
- A day is 24h ("1d ago" never appeared).
The row:
- The controls are last in the markup, so the booth's name comes first in
  tab order and wipe last. The cluster is placed over the strip from the
  row's box.
The theme:
- A choice made in one tab moves the Booth's other open tabs.
- The theme mark goes only on ask fragments the embed mounted.
Tests, strengthened after the code review:
- the pill is visible at rest;
- keyboard focus reveals the controls;
- the controls act with scripts off;
- Reveal all reaches the doc page;
- the high-contrast check reads tokens that actually differ;
- the art-light extras are written from SVOS, not derived from the
  copies;
- two overstated mutation rows are replaced (one was a runtime no-op, one
  went red through a syntax error).
Contract amended.

r2b.toml 55/55 proved. 799 passed.
This commit is contained in:
vh
2026-09-23 20:03:32 -07:00
parent 436d234ca0
commit 1558a7fa07
9 changed files with 446 additions and 84 deletions
+27 -6
View File
@@ -261,29 +261,50 @@ def human_dur(seconds: float) -> str:
_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
# NEVER RAISE: the Desk renders every row in ONE response, and a clock a fleet
# session can set (content mtimes) or a filesystem can report (birth time) may
# lie outside the calendar's range — which raised through the template and
# 500'd the index for every booth (heid bug-hunt, 4/4). A date that cannot be
# rendered is "" and the macro renders nothing, as for an unknown birth time.
_BAD_DATE = (OverflowError, OSError, ValueError)
def date_iso(epoch: float) -> str:
return datetime.fromtimestamp(epoch, tz=timezone.utc).isoformat(timespec="seconds")
try:
return datetime.fromtimestamp(epoch, tz=timezone.utc).isoformat(timespec="seconds")
except _BAD_DATE:
return ""
def date_stamp(epoch: float) -> str:
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
try:
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
except _BAD_DATE:
return ""
def date_day(epoch: float, now: float | None = None) -> str:
"""'12 Sep', with the year only when it is not this year's."""
t, n = time.localtime(epoch), time.localtime(time.time() if now is None else now)
"""'12 Sep', with the year only when it is not this year's; '' when the
calendar cannot hold it."""
try:
t, n = time.localtime(epoch), time.localtime(time.time() if now is None else now)
except _BAD_DATE:
return ""
day = f"{t.tm_mday} {_MONTHS[t.tm_mon - 1]}"
return day if t.tm_year == n.tm_year else f"{day} {t.tm_year}"
def date_ago(seconds: float) -> str:
"""An age in its largest whole unit: 'just now', '12m ago', '5h ago', '3d ago'."""
"""An age in its largest whole unit: 'just now', '12m ago', '5h ago',
'1d ago'. A day is 24h (hours used to run on to 47h, so '1d ago' never
appeared). The caller never passes a clock AHEAD of now: that is said as a
date, not an age."""
s = int(seconds)
if s < 60:
return "just now"
if s < 3600:
return f"{s // 60}m ago"
if s < 2 * 86400:
if s < 86400:
return f"{s // 3600}h ago"
return f"{s // 86400}d ago"
+11 -2
View File
@@ -348,12 +348,19 @@
choice (the same localStorage key the Booth's toggle writes — same
origin). Absent or unreadable = follow the OS, as before. The host page's
own <html> is the author's and is never touched. */
var ours = []; // every element this script mounted
function bkTheme() {
var t = null;
try { t = localStorage.getItem("booth.theme"); } catch (e) {}
var forced = t === "light" || t === "dark";
Array.prototype.forEach.call(document.querySelectorAll(".bk-ask"), function (el) {
if (forced) el.setAttribute("data-bk-theme", t); else el.removeAttribute("data-bk-theme");
/* OUR fragments only (heid bug-hunt): an author's own `.bk-ask` in the
host page is theirs, and is never marked. */
ours.forEach(function (root) {
var els = [root].concat(Array.prototype.slice.call(root.querySelectorAll(".bk-ask")));
els.forEach(function (el) {
if (!el.classList.contains("bk-ask")) return;
if (forced) el.setAttribute("data-bk-theme", t); else el.removeAttribute("data-bk-theme");
});
});
}
/* A choice made in another tab moves an open report live. */
@@ -376,6 +383,8 @@
if (!data) return;
favicon(data.favicon);
var mounted = place(data.marks || []);
ours = [];
Object.keys(mounted).forEach(function (id) { ours = ours.concat(mounted[id]); });
bkTheme();
reassociate();
asksChip(data.open || [], mounted);
+16 -6
View File
@@ -1,11 +1,21 @@
{# r2b D1b — a booth's two dates, defined ONCE for the Desk row and the booth
header. Created is the day it began (the filesystem's birth time); updated
is how recently its CONTENT moved (`landed_at`, the clock "new since you
looked" reads). Each is a <time> with its exact stamp as the title. A birth
time the filesystem cannot give renders nothing — never a plausible guess.
"Updated" is left out when it is within a minute of "created": a booth that
arrived in one go has one date, not two that say the same thing. #}
looked" reads). Each is a <time> with its exact stamp as the title.
- A date the filesystem cannot give, or the calendar cannot hold, renders
NOTHING — never a plausible guess, and never a 500 for the whole Desk.
- "Updated" shows whenever it differs from "created" by a minute or more,
EITHER way: copied files keep their mtimes while the folder is born now,
so content can be older than its booth. Within a minute, one date.
- A content clock AHEAD of now (a wrong clock somewhere) is said as its
date, never as an age — "updated just now" would be a lie. #}
{% macro dates(created_at, landed_at, now) -%}
{%- if created_at %} · <time class="d-made" datetime="{{ created_at|iso }}" title="created {{ created_at|stamp }}">created {{ created_at|day(now) }}</time>{% endif -%}
{%- if landed_at and (not created_at or landed_at - created_at > 60) %} · <time class="d-upd" datetime="{{ landed_at|iso }}" title="updated {{ landed_at|stamp }}">updated {{ (now - landed_at)|ago }}</time>{% endif -%}
{%- set made = created_at|day(now) if created_at else "" -%}
{%- if made %} · <time class="d-made" datetime="{{ created_at|iso }}" title="created {{ created_at|stamp }}">created {{ made }}</time>{% endif -%}
{%- set moved = landed_at|day(now) if landed_at else "" -%}
{%- if moved and (not made or (landed_at - created_at)|abs >= 60) -%}
{%- set age = now - landed_at -%}
{%- if age < -60 %} · <time class="d-upd" datetime="{{ landed_at|iso }}" title="updated {{ landed_at|stamp }}">updated {{ moved }}</time>
{%- else %} · <time class="d-upd" datetime="{{ landed_at|iso }}" title="updated {{ landed_at|stamp }}">updated {{ age|ago }}</time>{% endif -%}
{%- endif -%}
{%- endmacro %}
+26 -11
View File
@@ -244,14 +244,15 @@
.life .held,.life .held-broken{color:inherit}
.desk-facts time{white-space:nowrap}
/* THE ROW'S CONTROLS (r2b D1). .desk-media holds the strip and .desk-acts.
DEFAULT — no real hover (touch, any coarse pointer, or no hover at all):
the box dissolves and the cluster is the row's last line, visible, every
control at least the 28px floor (32px). Hover-only would mean no controls
at all on touch; with scripts off no confirm fires, so wipe stands apart. */
.desk-row{flex-wrap:wrap}
.desk-media{display:contents}
.desk-acts{order:99;flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px}
/* THE ROW'S CONTROLS (r2b D1), last in the row's markup. DEFAULT — no real
hover (touch, any coarse pointer, or no hover at all): the cluster is the
row's last line, visible, every control at least the 28px floor (32px).
Hover-only would mean no controls at all on touch; with scripts off no
confirm fires, so wipe stands apart. The strip keeps to the row's top, so
the hover cluster below can be placed over it from the row's own box. */
.desk-row{flex-wrap:wrap;position:relative}
.desk-strip{align-self:flex-start;min-width:0}
.desk-acts{flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px}
.desk-acts form{margin:0}
.desk-acts .dl-link,.desk-acts button{display:inline-flex;align-items:center;justify-content:center;
min-height:32px;min-width:32px;padding:0 10px;font-family:var(--font-mono);font-size:var(--size-caption);
@@ -272,8 +273,9 @@
so hidden controls stay in the tab order and focusing one shows them. */
@media (hover:hover) and (pointer:fine) and (not (any-pointer:coarse)){
.desk-row{flex-wrap:nowrap}
.desk-media{display:block;position:relative;flex:0 0 210px}
.desk-acts{position:absolute;top:5px;right:5px;flex:none;gap:4px;padding:3px;
/* over the strip's top-right corner: the strip starts at the row's 12px
padding and is 210px wide */
.desk-acts{position:absolute;top:17px;left:calc(12px + 210px - 5px);transform:translateX(-100%);flex:none;gap:4px;padding:3px;
background:color-mix(in oklab,var(--surface-overlay) 90%,transparent);
border:1px solid var(--border-default);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);
opacity:0;pointer-events:none;transition:opacity var(--dur-1) var(--ease-out)}
@@ -281,9 +283,13 @@
.desk-acts .dl-link,.desk-acts button{min-height:28px;min-width:28px;padding:0 7px}
.desk-acts .wipe{margin-left:5px}
}
@media (hover:hover) and (pointer:fine) and (not (any-pointer:coarse)) and (max-width:700px){
/* the strip spans the row: its top-right is the row's top-right */
.desk-acts{left:auto;right:17px;transform:none}
}
@media (max-width:700px){
.desk-row{flex-wrap:wrap}
.desk-strip,.desk-media{flex-basis:100%}
.desk-strip{flex-basis:100%}
.desk-side{flex-basis:100%;flex-direction:row;align-items:center;flex-wrap:wrap}
}
/* the side column */
@@ -1144,6 +1150,15 @@
}
box.hidden = false;
show();
/* A choice made in another tab moves this one too (heid bug-hunt): the
embed's ask chrome already followed; the Booth's own pages did not. */
window.addEventListener('storage', function (e) {
if (e.key !== 'booth.theme' && e.key !== null) return;
var t = null;
try { t = localStorage.getItem('booth.theme'); } catch (x) {}
if (t === 'light' || t === 'dark') d.setAttribute('data-theme', t); else d.removeAttribute('data-theme');
show();
});
box.addEventListener('click', function (ev) {
var b = ev.target.closest ? ev.target.closest('[data-theme-choice]') : null;
if (!b) return;
+23 -25
View File
@@ -27,31 +27,7 @@
{% macro row(b, section) -%}
<article class="desk-row{% if section == 'needs' %} is-needs{% endif %}" data-booth="{{ b.name }}" data-kept="{{ '1' if b.kept else '0' }}">
{# The strip and the row's controls share one box (r2b D1). Where a real
hover exists it is the positioning box, and the controls float over the
strip's top-right corner — covering pictures, never information — and
appear only on hover or keyboard focus (operator: "download, keep and
release buttons only appear on mouseover"; x hides too, his answer to
the open point). Anywhere else the box dissolves (display: contents) and
the controls are the row's last line, visible: hover-only would mean no
controls at all on touch. Order: zip, keep or release, then wipe set
apart — zip out of the middle (operator), release still next to x. #}
<div class="desk-media">
{{ preview(b) }}
<div class="desk-acts">
<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" method="post" action="/b/{{ b.name_url }}/unkeep"
data-booth="{{ b.name }}" data-confirm="release"><button title="release this booth so it can be wiped">release</button></form>
<form class="wipe wipe-kept" method="post" action="/b/{{ b.name_url }}/delete"
data-booth="{{ b.name }}" data-confirm="wipe-kept"><button title="wipe this KEPT booth now" aria-label="wipe kept booth">× wipe</button></form>
{%- else %}
<form class="keepit" method="post" action="/b/{{ b.name_url }}/keep"><button title="keep — exempt from the {{ ttl_hours }}h sweep" aria-label="keep booth">★ keep</button></form>
<form class="wipe" method="post" action="/b/{{ b.name_url }}/delete"
data-booth="{{ b.name }}" data-confirm="wipe"><button title="wipe now" aria-label="wipe booth">× wipe</button></form>
{%- endif %}
</div>
</div>
{{ preview(b) }}
<div class="desk-main">
{# The manifest title leads when there is one; the directory name stays
beside it because it is what the URL says. #}
@@ -82,6 +58,28 @@
{% if b.booth_blurred %}<span class="badge badge-blur" title="the whole booth is blurred — cosmetic only">◉ blurred</span>{% endif %}
<span class="life {{ 'life-kept' if b.kept else ('life-held' if b.hold in ('open', 'unreadable') else 'life-count') }}">{{ lifetime(b.kept, b.hold, b.expires_in) }}</span>
</div>
{# The row's controls, LAST in the markup so the booth's name comes first
in tab order (heid bug-hunt: wipe used to be reachable before the booth
it acts on). Where a real hover exists they float over the strip's
top-right corner — covering pictures, never information — and appear
only on hover or keyboard focus (operator: "download, keep and release
buttons only appear on mouseover"; x hides too, his answer). Anywhere
else they are the row's last line, visible: hover-only would mean no
controls at all on touch. Order: zip, keep or release, then wipe set
apart — zip out of the middle (operator), release still next to x. #}
<div class="desk-acts">
<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" method="post" action="/b/{{ b.name_url }}/unkeep"
data-booth="{{ b.name }}" data-confirm="release"><button title="release this booth so it can be wiped">release</button></form>
<form class="wipe wipe-kept" method="post" action="/b/{{ b.name_url }}/delete"
data-booth="{{ b.name }}" data-confirm="wipe-kept"><button title="wipe this KEPT booth now" aria-label="wipe kept booth">× wipe</button></form>
{%- else %}
<form class="keepit" method="post" action="/b/{{ b.name_url }}/keep"><button title="keep — exempt from the {{ ttl_hours }}h sweep" aria-label="keep booth">★ keep</button></form>
<form class="wipe" method="post" action="/b/{{ b.name_url }}/delete"
data-booth="{{ b.name }}" data-confirm="wipe"><button title="wipe now" aria-label="wipe booth">× wipe</button></form>
{%- endif %}
</div>
</article>
{%- endmacro %}
@@ -77,9 +77,14 @@ on mouseover"*.
top-right, which is still picture.
- **Everywhere else (no hover, a coarse primary pointer, or any coarse pointer
present), the cluster is visible and in flow**, on its own line at the
bottom of the row. The mechanism: the strip and the cluster share one
`.desk-media` box. Under a real hover it is the positioning box; otherwise it
dissolves (`display: contents`) and the cluster becomes the row's last line.
bottom of the row.
- **The cluster is LAST in the row's markup**, so the booth's name comes first
in tab order and wipe comes last. Where a real hover exists it is placed over
the strip from the row's own box: the strip keeps to the row's top, 210px
wide from the row's 12px padding.
- Accepted: the hover query uses Media Queries 4 `not (...)`. An engine
without MQ4 drops the whole query, and the failure is the safe
direction: the controls show, in flow.
- Known limit: the touch-LAPTOP branch (a fine primary pointer plus a coarse
one) cannot be emulated, because Chromium's touch emulation makes the
primary pointer coarse. That branch is covered by reading the media query,
@@ -112,9 +117,16 @@ controls (actions). One macro, `_dates.html`, serves both:
(`now` in the context), so every row is measured from the same instant;
- each is a `<time>` with `datetime` (ISO) and its exact local stamp as the
title;
- **updated is left out** within a minute of created: a booth that arrived in
one go has one date, not two that say the same thing;
- a birth time the filesystem cannot give renders NOTHING.
- **updated shows whenever it differs from created by a minute or more,
EITHER way.** Copied files keep their mtimes while the folder is born now,
so content can be older than its booth. Within a minute, one date;
- a content clock AHEAD of now is said as its DATE, never as an age ("updated
just now" would be false);
- an age is in its largest whole unit, a day being 24h;
- a date the filesystem cannot give, or the calendar cannot hold, renders
NOTHING. The filters never raise: the Desk renders every row in one
response, so one unrenderable clock would otherwise 500 the index for every
booth.
## D2 — Reveal all (blur ruling A)
@@ -223,6 +235,8 @@ reading; this is the control the operator uses, which the blur ruling assumed.
- **Mechanism: `data-theme` on `<html>`.** Absent = the OS preference, exactly
today's sheet. An early `<head>` script sets it before first paint, so a
forced theme never flashes the other one.
- **A choice made in another tab moves every open Booth page** (the `storage`
event), as it moves the ask chrome.
- **System is live-following BY CONSTRUCTION.** Choosing System removes
`data-theme`, and the `prefers-color-scheme` media query takes over. A media
query tracks the OS live, so no `matchMedia` listener is needed: JS never
@@ -249,11 +263,15 @@ reading; this is the control the operator uses, which the blur ruling assumed.
old lacked. The committed test checks each re-scoped copy against the
UNMOVED dark block. SVOS's light and dark declare the same 42 properties,
and its dark-hc and light-hc the same 41, so a declaration the transform
drops fails it.
drops fails it. Light declares every dark-block property PLUS the art
layer's four light-only values (the three shadows and the armed glow).
That extra set is written in the test from SVOS, never derived from the
copies, so dropping it from both copies fails too.
- **No JS: no toggle, and the page follows the OS**, as today.
- **The toggle reaches inside verbatim pages** (operator: "theme toggle reaches
inside"). `embed.js` reads the same `localStorage["booth.theme"]` (the same
origin) and marks each `.bk-ask` it injects with `data-bk-theme`. Its
origin) and marks each `.bk-ask` IT MOUNTED (never an author's own element
of that class) with `data-bk-theme`. Its
colours follow that attribute exactly as the Booth's own sheet follows
`data-theme`: forced when set, OS when absent. It follows a change made in
another tab through the `storage` event. It sets nothing on the host page's
@@ -314,11 +332,25 @@ reading; this is the control the operator uses, which the blur ruling assumed.
pressing Light/Dark changes `--surface-base` and survives a reload (new page,
same context); System plus an emulated OS scheme flip changes it WITHOUT a
reload.
- `a_forced_theme_follows_high_contrast`: forced dark + `prefers-contrast:
more` resolves dark-hc's surface token.
- `a_forced_theme_follows_high_contrast`: under `prefers-contrast: more`,
forced dark resolves exactly what OS dark does, and forced light what OS
light does. The check reads tokens that DIFFER between a theme and its
high-contrast variant (`--text-faint`, `--border-default`; `--surface-card`
is the same in both and would prove nothing), and asserts that high contrast
actually changed them.
- `a_forced_theme_and_the_os_theme_are_the_same_declarations`: each light copy
equals the other and declares exactly the dark block's property set; each
light-hc copy equals the other and declares exactly the dark-hc block's.
equals the other and declares the dark block's property set plus the four
art-light values; each light-hc copy equals the other and declares exactly
the dark-hc block's.
- `created_and_updated_are_dated_facts_and_none_says_nothing`,
`updated_shows_whenever_it_differs_from_created_and_a_future_one_says_its_date`,
`a_date_no_calendar_can_hold_renders_nothing_and_never_500s`,
`an_age_is_said_in_its_largest_whole_unit` (D1b).
- `a_rows_booth_name_comes_before_its_controls_in_tab_order`,
`the_pill_shows_at_rest_and_focus_reveals_the_controls`,
`with_scripts_off_a_rows_controls_still_act` (D1).
- `a_theme_chosen_in_one_tab_moves_the_others`,
`the_theme_marks_only_the_ask_fragments_we_mounted` (D3).
## Assertions that change (declared before the code)
+121 -19
View File
@@ -311,18 +311,18 @@ 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:5px;right:5px;flex:none;gap:4px;padding:3px;'''
.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:5px;right:5px;flex:none;gap:4px;padding:3px;'''
.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{order:99;flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px}'''
.desk-acts{flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px}'''
new = '''
.desk-acts{order:99;flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px;opacity:0}'''
.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"
@@ -338,35 +338,33 @@ 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"'''
<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"'''
{%- 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-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 = '''
{%- if created_at %} · <time class="d-made" datetime="{{ created_at|iso }}" title="created {{ created_at|stamp }}">created {{ created_at|day(now) }}</time>{% endif -%}'''
new = '''
{%- set created_at = created_at or landed_at %}{%- if created_at %} · <time class="d-made" datetime="{{ created_at|iso }}" title="created {{ created_at|stamp }}">created {{ created_at|day(now) }}</time>{% endif -%}'''
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 {{ (now - landed_at)|ago }}'''
old = '''updated {{ age|ago }}'''
new = '''updated {{ (now - created_at)|ago }}'''
[[mutation]]
@@ -418,13 +416,18 @@ new = '''
if (c === 'system')'''
[[mutation]]
label = "D3 the stored theme is read after the stylesheet (a flash)"
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 = '''
var t = localStorage.getItem('booth.theme');'''
<script>
(function () {
var d = document.documentElement, b = d.getAttribute('data-booth');'''
new = '''
var t = localStorage.getItem('booth.' + 'theme');'''
<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"
@@ -444,3 +447,102 @@ 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}'''
+55 -2
View File
@@ -1045,8 +1045,13 @@ def test_a_forced_theme_and_the_os_theme_are_the_same_declarations():
assert "color-scheme: dark" in dark and len(dark) > 30 # the parser found the real dark block
assert os_light == forced_light
assert os_lhc == forced_lhc
assert props(forced_light) == props(dark) | (props(forced_light) & props(art))
assert props(dark) <= props(forced_light)
# Light overrides every dark property plus the art layer's four light-only
# values. That extra set is written here, from SVOS, NOT derived from the
# copies: deriving it from them let a transform that dropped an art-light
# value from BOTH copies pass (heid code-review, 2/4).
art_light = {"--shadow-sm", "--shadow-md", "--shadow-lg", "--glow-armed"}
assert art_light <= props(art)
assert props(forced_light) == props(dark) | art_light
assert props(forced_lhc) == props(dhc)
@@ -1064,11 +1069,14 @@ def test_the_lifetime_pill_class_is_kept_held_or_counting(tmp_path):
h = _booth(tmp_path, "held", {"a.png": PNG})
declare_pick(h, "q", {"prompt": "Which?", "options": ["x", "y"]})
_booth(tmp_path, "loose", {"a.png": PNG})
broken = _booth(tmp_path, "broken", {"a.png": PNG})
(broken / ".marks.json").write_text("{not json")
body = _client(tmp_path).get("/").text
pill = lambda n: re.search(r'<span class="life (life-\w+)"[^>]*>(.*?)</span>\s*</div>', _row(body, n), re.S)
assert pill("kept1").group(1) == "life-kept" and "kept" in pill("kept1").group(2)
assert pill("held").group(1) == "life-held" and "held until answered" in pill("held").group(2)
assert pill("loose").group(1) == "life-count" and "expires in" in pill("loose").group(2)
assert pill("broken").group(1) == "life-held" and "marks unreadable" in pill("broken").group(2)
# and the lifetime left the facts line, which is facts only
assert "expires in" not in re.search(r'class="desk-facts".*?</div>', _row(body, "loose"), re.S).group(0)
@@ -1108,3 +1116,48 @@ def test_a_forced_theme_is_applied_before_first_paint(tmp_path):
head = page[:page.index("</head>")]
assert head.index("booth.theme") < head.index("<style>"), url
assert re.search(r'<div class="theme"[^>]*hidden', page), url
def test_a_date_no_calendar_can_hold_renders_nothing_and_never_500s(tmp_path, monkeypatch):
"""heid bug-hunt (4/4): the date filters raised on a timestamp outside the
calendar's range, and the Desk renders every row in one response — one
poisoned clock 500'd the index for every booth. A date that cannot be
rendered renders nothing, like a birth time the disk cannot give."""
import booth.app as app_mod
_booth(tmp_path, "g", {"a.png": PNG})
_booth(tmp_path, "ok", {"a.png": PNG})
monkeypatch.setattr(app_mod, "birth_time", lambda p: 1e20 if p.name == "g" else None)
monkeypatch.setattr(app_mod, "_content_mtime", lambda p: -1e20 if p.name == "g" else time.time() - 3600)
c = _client(tmp_path)
r = c.get("/")
assert r.status_code == 200
assert "created" not in re.search(r'class="desk-facts".*?</div>', _row(r.text, "g"), re.S).group(0)
assert c.get("/b/g/").status_code == 200
@pytest.mark.parametrize("age,words", [(30, "just now"), (59 * 60, "59m ago"), (23 * 3600, "23h ago"),
(30 * 3600, "1d ago"), (47 * 3600, "1d ago"), (5 * 86400, "5d ago")])
def test_an_age_is_said_in_its_largest_whole_unit(age, words):
"""heid bug-hunt (groa): hours ran on to 47h, so "1d ago" never appeared,
against the helper's own docstring."""
from booth.app import date_ago
assert date_ago(age) == words
def test_updated_shows_whenever_it_differs_from_created_and_a_future_one_says_its_date(tmp_path, monkeypatch):
"""heid bug-hunt (hulda, groa): "updated" was dropped for content OLDER than
the booth — copied files keep their mtimes while the folder is born now —
and a future content clock read "updated just now". Either direction of a
real gap shows; a clock ahead of now shows its date, never an age."""
import booth.app as app_mod
now = time.time()
_booth(tmp_path, "copied", {"a.png": PNG})
_booth(tmp_path, "ahead", {"a.png": PNG})
monkeypatch.setattr(app_mod, "birth_time", lambda p: now - 3600)
monkeypatch.setattr(app_mod, "_content_mtime",
lambda p: now - 30 * 86400 if p.name == "copied" else now + 5 * 86400)
body = _client(tmp_path).get("/").text
facts = lambda n: re.search(r'class="desk-facts".*?</div>', _row(body, n), re.S).group(0)
assert re.search(r">updated 30d ago</time>", facts("copied"))
ahead = facts("ahead")
assert "just now" not in ahead and re.search(r">updated \d{1,2} [A-Z][a-z]{2}( \d{4})?</time>", ahead)
+123 -1
View File
@@ -898,11 +898,19 @@ def test_a_forced_theme_follows_high_contrast(browser, live):
cdp.send("Emulation.setEmulatedMedia", {"features": [
{"name": "prefers-contrast", "value": "more"},
{"name": "prefers-color-scheme", "value": os_scheme}]})
got[(os_scheme, forced)] = page.evaluate(_VAR, "--surface-card")
# tokens that DIFFER between a theme and its high-contrast variant
# (--surface-card does not, so reading it saw nothing — heid, 2/4)
got[(os_scheme, forced)] = (page.evaluate(_VAR, "--text-faint"), page.evaluate(_VAR, "--border-default"))
if forced is None and os_scheme == "dark":
cdp.send("Emulation.setEmulatedMedia", {"features": [
{"name": "prefers-contrast", "value": "no-preference"},
{"name": "prefers-color-scheme", "value": os_scheme}]})
got["dark, no contrast"] = (page.evaluate(_VAR, "--text-faint"), page.evaluate(_VAR, "--border-default"))
ctx.close()
assert got[("light", "dark")] == got[("dark", None)] == got[("dark", "dark")]
assert got[("dark", "light")] == got[("light", None)] == got[("light", "light")]
assert got[("dark", None)] != got[("light", None)]
assert got[("dark", None)] != got["dark, no contrast"] # high contrast really applied
def test_the_theme_toggle_never_shows_without_js_and_a_storage_failure_still_applies(browser, live):
@@ -961,3 +969,117 @@ def test_a_forced_theme_reaches_the_ask_chrome_inside_a_verbatim_page(browser, l
ctx.close()
assert os_dark == "#b2cd12" and forced_light == "#586519" and after_reload == "#586519", (os_dark, forced_light, after_reload)
assert host_html is None
def test_a_rows_booth_name_comes_before_its_controls_in_tab_order(browser, live):
"""heid bug-hunt (groa): the cluster sat before the text in the markup, so
the first tab stop on a row was zip, then keep, then WIPE — before the booth
it acts on — and with scripts off, wipe submits with no confirm. The name
is first; the controls follow, wipe last."""
base, root = live
_two_rows(root)
ctx = browser.new_context(viewport={"width": 390, "height": 844}, has_touch=True, is_mobile=True)
page = ctx.new_page()
page.goto(f"{base}/", wait_until="networkidle")
order = page.evaluate("""() => { const row = document.querySelector('.desk-row[data-booth="loose"]');
return [...row.querySelectorAll('a[href], button')].filter(e => e.tabIndex >= 0)
.map(e => e.classList.contains('desk-title') ? 'title' : e.closest('form') ? e.closest('form').className.split(' ')[0] : e.className.split(' ')[0]); }""")
ctx.close()
assert order == ["title", "dl-link", "keepit", "wipe"], order
def test_a_theme_chosen_in_one_tab_moves_the_others(browser, live):
"""heid bug-hunt (groa): the ask chrome followed a choice made in another
tab, but the Booth's own open pages did not until reloaded."""
base, root = live
_set(root, 1)
ctx = browser.new_context(color_scheme="dark")
a, b = ctx.new_page(), ctx.new_page()
a.goto(f"{base}/", wait_until="networkidle")
b.goto(f"{base}/b/g/", wait_until="networkidle")
a.locator('.theme [data-theme-choice="light"]').click()
b.wait_for_function("document.documentElement.getAttribute('data-theme') === 'light'", timeout=5000)
pressed = b.locator('.theme [aria-pressed="true"]').get_attribute("data-theme-choice")
a.locator('.theme [data-theme-choice="system"]').click()
b.wait_for_function("!document.documentElement.hasAttribute('data-theme')", timeout=5000)
ctx.close()
assert pressed == "light"
def test_the_theme_marks_only_the_ask_fragments_we_mounted(browser, live):
"""heid bug-hunt (regin): the theme mark went on every `.bk-ask` in the
page, the author's own included. Only fragments the embed mounted."""
from booth.marks import declare_pick
base, root = live
b = root / "rep"
b.mkdir()
declare_pick(b, "winner", {"prompt": "Which?", "options": ["A", "B"]})
(b / "index.html").write_text('<!doctype html><title>r</title><body><div class="bk-ask" id="authors">mine</div>'
'<script src="/_booth/embed.js" defer></script></body>')
ctx = browser.new_context()
page = ctx.new_page()
page.add_init_script("try { localStorage.setItem('booth.theme', 'light'); } catch (e) {}")
page.goto(f"{base}/b/rep/", wait_until="networkidle")
page.wait_for_selector(".bk-ask:not(#authors)")
got = page.evaluate("""() => [document.getElementById('authors').getAttribute('data-bk-theme'),
[...document.querySelectorAll('.bk-ask:not(#authors)')].map(e => e.getAttribute('data-bk-theme'))]""")
ctx.close()
assert got[0] is None and got[1] and all(t == "light" for t in got[1]), got
def test_the_pill_shows_at_rest_and_focus_reveals_the_controls(browser, live):
"""heid code-review: the pill's "visible with no hover" was never asserted
(an opacity-0 box keeps its geometry), and nothing pinned KEYBOARD focus
revealing the controls — `:focus-within` could go alone."""
base, root = live
_two_rows(root)
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto(f"{base}/", wait_until="networkidle")
page.mouse.move(0, 0)
row = page.locator('.desk-row[data-booth="loose"]')
pill = row.locator(".life").evaluate("e => [getComputedStyle(e).opacity, getComputedStyle(e).visibility, e.offsetWidth > 0]")
row.locator(".desk-acts .dl-link").focus()
page.wait_for_timeout(350)
focused = row.locator(".desk-acts").evaluate("a => [getComputedStyle(a).opacity, getComputedStyle(a).pointerEvents]")
page.close()
assert pill == ["1", "visible", True], pill
assert focused == ["1", "auto"], focused
def test_with_scripts_off_a_rows_controls_still_act(browser, live):
"""r2b INV-2, never exercised with JS actually off (heid code-review): the
hover reveal is CSS, and keep is a plain form."""
base, root = live
_two_rows(root)
ctx = browser.new_context(java_script_enabled=False, viewport={"width": 1400, "height": 900})
page = ctx.new_page()
page.goto(f"{base}/", wait_until="networkidle")
row = page.locator('.desk-row[data-booth="loose"]')
row.hover()
page.wait_for_timeout(350)
with page.expect_navigation():
row.locator(".desk-acts form.keepit button").click()
ctx.close()
assert (root / "loose" / ".forever").exists()
def test_reveal_all_lifts_the_doc_page_it_reaches(browser, live):
"""heid code-review (regin, seat-settled): the doc page carries the rules,
but no test turned Reveal all on and looked there."""
from booth.app import set_blurred
base, root = live
b = root / "g"
b.mkdir()
(b / "a.png").write_bytes(PNG)
(b / "n.md").write_text("# secret\n\nbody")
set_blurred(b, "a.png", True)
set_blurred(b, "n.md", True)
page = browser.new_page(viewport={"width": 1200, "height": 800})
page.goto(f"{base}/b/g/", wait_until="networkidle")
page.locator("[data-reveal-all]").click()
page.goto(f"{base}/b/g/view?f=n.md", wait_until="networkidle")
_settle(page)
got = [page.evaluate(_FILTER, "#docbody .markdown-body, #docbody .textview"),
page.evaluate("getComputedStyle(document.getElementById('docreveal')).display")]
page.close()
assert got == ["none", "none"], got