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:
+24
-3
@@ -261,29 +261,50 @@ def human_dur(seconds: float) -> str:
|
|||||||
_MONTHS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
|
_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:
|
def date_iso(epoch: float) -> str:
|
||||||
|
try:
|
||||||
return datetime.fromtimestamp(epoch, tz=timezone.utc).isoformat(timespec="seconds")
|
return datetime.fromtimestamp(epoch, tz=timezone.utc).isoformat(timespec="seconds")
|
||||||
|
except _BAD_DATE:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def date_stamp(epoch: float) -> str:
|
def date_stamp(epoch: float) -> str:
|
||||||
|
try:
|
||||||
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
|
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:
|
def date_day(epoch: float, now: float | None = None) -> str:
|
||||||
"""'12 Sep', with the year only when it is not this year's."""
|
"""'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)
|
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]}"
|
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}"
|
return day if t.tm_year == n.tm_year else f"{day} {t.tm_year}"
|
||||||
|
|
||||||
|
|
||||||
def date_ago(seconds: float) -> str:
|
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)
|
s = int(seconds)
|
||||||
if s < 60:
|
if s < 60:
|
||||||
return "just now"
|
return "just now"
|
||||||
if s < 3600:
|
if s < 3600:
|
||||||
return f"{s // 60}m ago"
|
return f"{s // 60}m ago"
|
||||||
if s < 2 * 86400:
|
if s < 86400:
|
||||||
return f"{s // 3600}h ago"
|
return f"{s // 3600}h ago"
|
||||||
return f"{s // 86400}d ago"
|
return f"{s // 86400}d ago"
|
||||||
|
|
||||||
|
|||||||
+10
-1
@@ -348,13 +348,20 @@
|
|||||||
choice (the same localStorage key the Booth's toggle writes — same
|
choice (the same localStorage key the Booth's toggle writes — same
|
||||||
origin). Absent or unreadable = follow the OS, as before. The host page's
|
origin). Absent or unreadable = follow the OS, as before. The host page's
|
||||||
own <html> is the author's and is never touched. */
|
own <html> is the author's and is never touched. */
|
||||||
|
var ours = []; // every element this script mounted
|
||||||
function bkTheme() {
|
function bkTheme() {
|
||||||
var t = null;
|
var t = null;
|
||||||
try { t = localStorage.getItem("booth.theme"); } catch (e) {}
|
try { t = localStorage.getItem("booth.theme"); } catch (e) {}
|
||||||
var forced = t === "light" || t === "dark";
|
var forced = t === "light" || t === "dark";
|
||||||
Array.prototype.forEach.call(document.querySelectorAll(".bk-ask"), function (el) {
|
/* 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");
|
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. */
|
/* A choice made in another tab moves an open report live. */
|
||||||
window.addEventListener("storage", function (e) {
|
window.addEventListener("storage", function (e) {
|
||||||
@@ -376,6 +383,8 @@
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
favicon(data.favicon);
|
favicon(data.favicon);
|
||||||
var mounted = place(data.marks || []);
|
var mounted = place(data.marks || []);
|
||||||
|
ours = [];
|
||||||
|
Object.keys(mounted).forEach(function (id) { ours = ours.concat(mounted[id]); });
|
||||||
bkTheme();
|
bkTheme();
|
||||||
reassociate();
|
reassociate();
|
||||||
asksChip(data.open || [], mounted);
|
asksChip(data.open || [], mounted);
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
{# r2b D1b — a booth's two dates, defined ONCE for the Desk row and the booth
|
{# 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
|
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
|
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
|
looked" reads). Each is a <time> with its exact stamp as the title.
|
||||||
time the filesystem cannot give renders nothing — never a plausible guess.
|
- A date the filesystem cannot give, or the calendar cannot hold, renders
|
||||||
"Updated" is left out when it is within a minute of "created": a booth that
|
NOTHING — never a plausible guess, and never a 500 for the whole Desk.
|
||||||
arrived in one go has one date, not two that say the same thing. #}
|
- "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) -%}
|
{% 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 -%}
|
{%- set made = created_at|day(now) if created_at else "" -%}
|
||||||
{%- 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 -%}
|
{%- 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 %}
|
{%- endmacro %}
|
||||||
|
|||||||
+26
-11
@@ -244,14 +244,15 @@
|
|||||||
.life .held,.life .held-broken{color:inherit}
|
.life .held,.life .held-broken{color:inherit}
|
||||||
.desk-facts time{white-space:nowrap}
|
.desk-facts time{white-space:nowrap}
|
||||||
|
|
||||||
/* THE ROW'S CONTROLS (r2b D1). .desk-media holds the strip and .desk-acts.
|
/* THE ROW'S CONTROLS (r2b D1), last in the row's markup. DEFAULT — no real
|
||||||
DEFAULT — no real hover (touch, any coarse pointer, or no hover at all):
|
hover (touch, any coarse pointer, or no hover at all): the cluster is the
|
||||||
the box dissolves and the cluster is the row's last line, visible, every
|
row's last line, visible, every control at least the 28px floor (32px).
|
||||||
control at least the 28px floor (32px). Hover-only would mean no controls
|
Hover-only would mean no controls at all on touch; with scripts off no
|
||||||
at all on touch; with scripts off no confirm fires, so wipe stands apart. */
|
confirm fires, so wipe stands apart. The strip keeps to the row's top, so
|
||||||
.desk-row{flex-wrap:wrap}
|
the hover cluster below can be placed over it from the row's own box. */
|
||||||
.desk-media{display:contents}
|
.desk-row{flex-wrap:wrap;position:relative}
|
||||||
.desk-acts{order:99;flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:6px}
|
.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 form{margin:0}
|
||||||
.desk-acts .dl-link,.desk-acts button{display:inline-flex;align-items:center;justify-content:center;
|
.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);
|
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. */
|
so hidden controls stay in the tab order and focusing one shows them. */
|
||||||
@media (hover:hover) and (pointer:fine) and (not (any-pointer:coarse)){
|
@media (hover:hover) and (pointer:fine) and (not (any-pointer:coarse)){
|
||||||
.desk-row{flex-wrap:nowrap}
|
.desk-row{flex-wrap:nowrap}
|
||||||
.desk-media{display:block;position:relative;flex:0 0 210px}
|
/* over the strip's top-right corner: the strip starts at the row's 12px
|
||||||
.desk-acts{position:absolute;top:5px;right:5px;flex:none;gap:4px;padding:3px;
|
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);
|
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);
|
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)}
|
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 .dl-link,.desk-acts button{min-height:28px;min-width:28px;padding:0 7px}
|
||||||
.desk-acts .wipe{margin-left:5px}
|
.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){
|
@media (max-width:700px){
|
||||||
.desk-row{flex-wrap:wrap}
|
.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}
|
.desk-side{flex-basis:100%;flex-direction:row;align-items:center;flex-wrap:wrap}
|
||||||
}
|
}
|
||||||
/* the side column */
|
/* the side column */
|
||||||
@@ -1144,6 +1150,15 @@
|
|||||||
}
|
}
|
||||||
box.hidden = false;
|
box.hidden = false;
|
||||||
show();
|
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) {
|
box.addEventListener('click', function (ev) {
|
||||||
var b = ev.target.closest ? ev.target.closest('[data-theme-choice]') : null;
|
var b = ev.target.closest ? ev.target.closest('[data-theme-choice]') : null;
|
||||||
if (!b) return;
|
if (!b) return;
|
||||||
|
|||||||
+22
-24
@@ -27,31 +27,7 @@
|
|||||||
|
|
||||||
{% macro row(b, section) -%}
|
{% 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' }}">
|
<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) }}
|
{{ 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>
|
|
||||||
<div class="desk-main">
|
<div class="desk-main">
|
||||||
{# The manifest title leads when there is one; the directory name stays
|
{# The manifest title leads when there is one; the directory name stays
|
||||||
beside it because it is what the URL says. #}
|
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 %}
|
{% 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>
|
<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>
|
</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>
|
</article>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,14 @@ on mouseover"*.
|
|||||||
top-right, which is still picture.
|
top-right, which is still picture.
|
||||||
- **Everywhere else (no hover, a coarse primary pointer, or any coarse pointer
|
- **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
|
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
|
bottom of the row.
|
||||||
`.desk-media` box. Under a real hover it is the positioning box; otherwise it
|
- **The cluster is LAST in the row's markup**, so the booth's name comes first
|
||||||
dissolves (`display: contents`) and the cluster becomes the row's last line.
|
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
|
- Known limit: the touch-LAPTOP branch (a fine primary pointer plus a coarse
|
||||||
one) cannot be emulated, because Chromium's touch emulation makes the
|
one) cannot be emulated, because Chromium's touch emulation makes the
|
||||||
primary pointer coarse. That branch is covered by reading the media query,
|
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;
|
(`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
|
- each is a `<time>` with `datetime` (ISO) and its exact local stamp as the
|
||||||
title;
|
title;
|
||||||
- **updated is left out** within a minute of created: a booth that arrived in
|
- **updated shows whenever it differs from created by a minute or more,
|
||||||
one go has one date, not two that say the same thing;
|
EITHER way.** Copied files keep their mtimes while the folder is born now,
|
||||||
- a birth time the filesystem cannot give renders NOTHING.
|
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)
|
## 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
|
- **Mechanism: `data-theme` on `<html>`.** Absent = the OS preference, exactly
|
||||||
today's sheet. An early `<head>` script sets it before first paint, so a
|
today's sheet. An early `<head>` script sets it before first paint, so a
|
||||||
forced theme never flashes the other one.
|
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
|
- **System is live-following BY CONSTRUCTION.** Choosing System removes
|
||||||
`data-theme`, and the `prefers-color-scheme` media query takes over. A media
|
`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
|
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
|
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,
|
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
|
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.
|
- **No JS: no toggle, and the page follows the OS**, as today.
|
||||||
- **The toggle reaches inside verbatim pages** (operator: "theme toggle reaches
|
- **The toggle reaches inside verbatim pages** (operator: "theme toggle reaches
|
||||||
inside"). `embed.js` reads the same `localStorage["booth.theme"]` (the same
|
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
|
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
|
`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
|
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,
|
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
|
same context); System plus an emulated OS scheme flip changes it WITHOUT a
|
||||||
reload.
|
reload.
|
||||||
- `a_forced_theme_follows_high_contrast`: forced dark + `prefers-contrast:
|
- `a_forced_theme_follows_high_contrast`: under `prefers-contrast: more`,
|
||||||
more` resolves dark-hc's surface token.
|
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
|
- `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
|
equals the other and declares the dark block's property set plus the four
|
||||||
light-hc copy equals the other and declares exactly the dark-hc block's.
|
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)
|
## Assertions that change (declared before the code)
|
||||||
|
|
||||||
|
|||||||
+115
-13
@@ -311,18 +311,18 @@ label = "D1 the cluster stays in flow where a hover exists (it takes room)"
|
|||||||
file = "booth/templates/base.html"
|
file = "booth/templates/base.html"
|
||||||
test = "tests/test_flow_browser.py::test_the_row_controls_take_no_room_where_a_hover_exists"
|
test = "tests/test_flow_browser.py::test_the_row_controls_take_no_room_where_a_hover_exists"
|
||||||
old = '''
|
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 = '''
|
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]]
|
[[mutation]]
|
||||||
label = "D1 hover-only everywhere (no controls at all on touch)"
|
label = "D1 hover-only everywhere (no controls at all on touch)"
|
||||||
file = "booth/templates/base.html"
|
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"
|
test = "tests/test_flow_browser.py::test_on_touch_the_row_controls_are_visible_in_flow_and_at_least_28px"
|
||||||
old = '''
|
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 = '''
|
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]]
|
[[mutation]]
|
||||||
label = "D1 touch controls fall below the 28px floor"
|
label = "D1 touch controls fall below the 28px floor"
|
||||||
@@ -351,22 +351,20 @@ label = "D1 a held booth's pill reads as counting down"
|
|||||||
file = "booth/templates/index.html"
|
file = "booth/templates/index.html"
|
||||||
test = "tests/test_flow.py::test_the_lifetime_pill_class_is_kept_held_or_counting"
|
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')'''
|
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]]
|
[[mutation]]
|
||||||
label = "D1b an unknown birth time renders a guess"
|
label = "D1b an unknown birth time renders a guess"
|
||||||
file = "booth/templates/_dates.html"
|
file = "booth/templates/_dates.html"
|
||||||
test = "tests/test_flow.py::test_created_and_updated_are_dated_facts_and_none_says_nothing"
|
test = "tests/test_flow.py::test_created_and_updated_are_dated_facts_and_none_says_nothing"
|
||||||
old = '''
|
old = '''{% 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 -%}'''
|
new = '''{% macro dates(created_at, landed_at, now) -%}{%- set created_at = created_at or landed_at -%}'''
|
||||||
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 -%}'''
|
|
||||||
|
|
||||||
[[mutation]]
|
[[mutation]]
|
||||||
label = "D1b updated is measured from the wrong clock"
|
label = "D1b updated is measured from the wrong clock"
|
||||||
file = "booth/templates/_dates.html"
|
file = "booth/templates/_dates.html"
|
||||||
test = "tests/test_flow.py::test_created_and_updated_are_dated_facts_and_none_says_nothing"
|
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 }}'''
|
new = '''updated {{ (now - created_at)|ago }}'''
|
||||||
|
|
||||||
[[mutation]]
|
[[mutation]]
|
||||||
@@ -418,13 +416,18 @@ new = '''
|
|||||||
if (c === 'system')'''
|
if (c === 'system')'''
|
||||||
|
|
||||||
[[mutation]]
|
[[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"
|
file = "booth/templates/base.html"
|
||||||
test = "tests/test_flow.py::test_a_forced_theme_is_applied_before_first_paint"
|
test = "tests/test_flow.py::test_a_forced_theme_is_applied_before_first_paint"
|
||||||
old = '''
|
old = '''
|
||||||
var t = localStorage.getItem('booth.theme');'''
|
<script>
|
||||||
|
(function () {
|
||||||
|
var d = document.documentElement, b = d.getAttribute('data-booth');'''
|
||||||
new = '''
|
new = '''
|
||||||
var t = localStorage.getItem('booth.' + 'theme');'''
|
<style></style>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var d = document.documentElement, b = d.getAttribute('data-booth');'''
|
||||||
|
|
||||||
[[mutation]]
|
[[mutation]]
|
||||||
label = "D3 the ask chrome in a verbatim page ignores the choice"
|
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();'''
|
if (e.key === "booth.theme" || e.key === null) bkTheme();'''
|
||||||
new = '''
|
new = '''
|
||||||
if (false) bkTheme();'''
|
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
@@ -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 "color-scheme: dark" in dark and len(dark) > 30 # the parser found the real dark block
|
||||||
assert os_light == forced_light
|
assert os_light == forced_light
|
||||||
assert os_lhc == forced_lhc
|
assert os_lhc == forced_lhc
|
||||||
assert props(forced_light) == props(dark) | (props(forced_light) & props(art))
|
# Light overrides every dark property plus the art layer's four light-only
|
||||||
assert props(dark) <= props(forced_light)
|
# 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)
|
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})
|
h = _booth(tmp_path, "held", {"a.png": PNG})
|
||||||
declare_pick(h, "q", {"prompt": "Which?", "options": ["x", "y"]})
|
declare_pick(h, "q", {"prompt": "Which?", "options": ["x", "y"]})
|
||||||
_booth(tmp_path, "loose", {"a.png": PNG})
|
_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
|
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)
|
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("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("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("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
|
# 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)
|
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>")]
|
head = page[:page.index("</head>")]
|
||||||
assert head.index("booth.theme") < head.index("<style>"), url
|
assert head.index("booth.theme") < head.index("<style>"), url
|
||||||
assert re.search(r'<div class="theme"[^>]*hidden', page), 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
@@ -898,11 +898,19 @@ def test_a_forced_theme_follows_high_contrast(browser, live):
|
|||||||
cdp.send("Emulation.setEmulatedMedia", {"features": [
|
cdp.send("Emulation.setEmulatedMedia", {"features": [
|
||||||
{"name": "prefers-contrast", "value": "more"},
|
{"name": "prefers-contrast", "value": "more"},
|
||||||
{"name": "prefers-color-scheme", "value": os_scheme}]})
|
{"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()
|
ctx.close()
|
||||||
assert got[("light", "dark")] == got[("dark", None)] == got[("dark", "dark")]
|
assert got[("light", "dark")] == got[("dark", None)] == got[("dark", "dark")]
|
||||||
assert got[("dark", "light")] == got[("light", None)] == got[("light", "light")]
|
assert got[("dark", "light")] == got[("light", None)] == got[("light", "light")]
|
||||||
assert got[("dark", None)] != got[("light", None)]
|
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):
|
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()
|
ctx.close()
|
||||||
assert os_dark == "#b2cd12" and forced_light == "#586519" and after_reload == "#586519", (os_dark, forced_light, after_reload)
|
assert os_dark == "#b2cd12" and forced_light == "#586519" and after_reload == "#586519", (os_dark, forced_light, after_reload)
|
||||||
assert host_html is None
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user