Files
booth/tests/test_antislop.py
T
vh 7143fae6c7 fix(as-S5a): fixup from booth-dev's gate — release asks, fail-closed words, ids that cannot collide
From booth-dev's hulda bug-hunt with heid's second voice (BRINGA, thread
01M3MVGQ7QSCCK8WT59TQ4J469):
- The booth page's "kept — release" asks by name, as the Desk's does.
- WORDS has no prototype: data-confirm="__proto__" or "constructor" is an
  unknown word, and asks, instead of throwing before preventDefault.
- The confirm helper moved into <head>: its capture listener exists
  before any form, so a click during load is asked too (the inline
  confirm() it replaced had that property).
- shown() also marks U+2028/U+2029 and the zero-width characters.
- Derived ids take ':' (bk-ask-<id>-<key>:prompt, bk-ask-<id>:title), which
  no id or key can contain; '-prompt' and '-title' collided with valid
  keys. booth-dev's chip test now looks its fragment up by [id=...].
- human_dur says "—" for a value that is not finite, instead of raising.
- The tile's copy of a note drops its id (booth-dev: mark-<id> is the
  panel's article).
- Four guards that asserted source patterns now also hold on computed
  effects: embed rings, rings inside clipping containers, the withdraw ×
  on both axes, and question-level notes fields.

Contract: as_antislop S5a (fixup). Falsifiers: antislop.toml 102/102 with
r2_flow.toml 24/24 proved; the full gate follows.
2026-09-28 13:58:52 -07:00

554 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The anti-slop fix slices (docs/contracts/as_antislop.contract.md).
S1, the house clock: a clock time the operator reads is local 24-hour time as
four digits with no colon (0848). Raw ISO stamps, HH:MM, microseconds, offsets
and a poster's IP address never reach visible text. The exact stored value
stays available in each <time>'s `datetime`.
"""
from __future__ import annotations
import os
import re
import time
import pytest
from booth.marks import answer_pick, declare_pick, marks_for, write_note
PACIFIC = "America/Los_Angeles"
@pytest.fixture(autouse=True)
def pacific():
"""Pin the zone: `clock` renders local time, and this box's local time is
the operator's. A test that inherited the runner's zone would pass or fail by
where it ran."""
old = os.environ.get("TZ")
os.environ["TZ"] = PACIFIC
time.tzset()
yield
if old is None:
os.environ.pop("TZ", None)
else:
os.environ["TZ"] = old
time.tzset()
def _now():
return time.mktime((2026, 9, 28, 12, 0, 0, 0, 0, -1))
@pytest.fixture
def client(tmp_path):
from fastapi.testclient import TestClient
from booth.app import create_app
return TestClient(create_app(tmp_path, ttl_hours=24, start_sweeper=False)), tmp_path
def _text(fragment: str) -> str:
return re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", fragment)).strip()
HHMM = re.compile(r"\b\d{1,2}:\d{2}\b")
ISO = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}")
IP = re.compile(r"\b\d{1,3}(?:\.\d{1,3}){3}\b|::1\b")
HOUSE = re.compile(r"\b\d{1,2} [A-Z][a-z]{2}(?: \d{4})? \d{4}\b")
# ---- the filters ---------------------------------------------------------------
def test_clock_forms():
from booth.app import clock
now = _now()
assert clock("2026-09-28T08:48:20.478024-07:00", now=now) == "28 Sep 0848"
assert clock("2026-09-28T15:48:20+00:00", now=now) == "28 Sep 0848" # converted to local
assert clock("2026-09-28T08:48:20", now=now) == "28 Sep 0848" # naive = local
assert clock("2026-09-06 23:35", now=now) == "6 Sep 2335" # the board's rows
assert clock("2025-09-06 23:35", now=now) == "6 Sep 2025 2335" # another year says so
assert clock(time.mktime((2026, 9, 28, 8, 48, 0, 0, 0, -1)), now=now) == "28 Sep 0848"
assert clock("", now=now) == "" and clock(None, now=now) == ""
assert clock("2026-09-28", now=now) == "28 Sep" # a date has no clock: no 0000
assert clock("2026-W39-1", now=now) == "21 Sep" # nor does an ISO week
def test_clock_never_raises():
"""The Desk and the board render many rows in ONE response: one bad stamp
must cost its own text, never the page. What cannot be read is shown as
given, never guessed."""
from booth.app import clock
for bad in ("not a time", "2026-13-45T99:99", "99999-01-01T00:00:00", 1e20, 10 ** 400, -(10 ** 400),
float("nan"), float("inf"), True, [1]):
assert isinstance(clock(bad, now=_now()), str)
assert clock("not a time", now=_now()) == "not a time"
assert clock("2026-13-45T99:99", now=_now()) == "2026-13-45T99:99"
def test_byline_hides_addresses():
from booth.app import byline
for addr in ("127.0.0.1", "10.100.10.5", "::1", "2001:db8::1", "10.100.10.5:443", "10.100.10.5/32",
"[2001:db8::1]:443", "\u200b10.100.10.5", " 127.0.0.1 "):
assert byline(addr) == "", addr
assert byline("design-dev") == "design-dev" and byline("host:8080") == "host:8080"
assert byline("") == "" and byline(None) == ""
def test_date_stamp_is_house_form():
from booth.app import date_stamp
assert re.fullmatch(r"\d{4}-\d{2}-\d{2} \d{4}", date_stamp(_now())), date_stamp(_now())
assert date_stamp(_now()) == "2026-09-28 1200"
# ---- where they apply ------------------------------------------------------------
def _answered_booth(data):
b = data / "b"
b.mkdir()
declare_pick(b, "p1", {"prompt": "Which?", "options": ["North", "South"]})
answer_pick(b, "p1", marks_for(b)[0].options[0]["id"], who="127.0.0.1")
write_note(b, None, "about the booth", who="10.100.10.5")
return b
def _whens(html):
return [_text(w) for w in re.findall(r'<span class="mark-when">(.*?)</span>', html, flags=re.S)]
@pytest.mark.parametrize("path", ["/b/b/marks", "/b/b/"])
def test_rendered_marks_use_the_house_clock(client, path):
c, data = client
_answered_booth(data)
html = c.get(path).text
whens = _whens(html)
assert whens, f"{path} renders no mark line at all (positive control)"
for w in whens:
assert HOUSE.search(w), w
assert not (HHMM.search(w) or ISO.search(w) or IP.search(w)), w
visible = _text(re.sub(r"<(script|style)\b.*?</\1>", " ", html, flags=re.S))
assert "127.0.0.1" not in html and "10.100.10.5" not in html, "no address anywhere, attributes included"
stored = {m.created for m in marks_for(data / "b")} | {m.answer["answered_at"] for m in marks_for(data / "b") if m.answer}
carried = set(re.findall(r'<time datetime="([^"]+)"', html))
assert stored & carried, "the exact stored stamp rides in datetime="
def test_embed_fragment_uses_the_house_clock(client):
c, data = client
_answered_booth(data)
(m,) = [m for m in c.get("/b/b/embed.json").json()["marks"] if m["id"] == "p1"]
# The SUBMIT block's tag carries the stamp (the per-question tag says only
# "✓ answered"); the embed places it from `submit`.
tag = _text(re.search(r'<span class="bk-ask-tag">(.*?)</span>', m["submit"], flags=re.S).group(1))
assert tag.startswith("✓ answered"), tag
assert HOUSE.search(tag) and not (ISO.search(tag) or HHMM.search(tag)), tag
def test_board_rows_use_the_house_clock(client):
from booth.links import LINKS_FILE
c, data = client
board = data / "links"
board.mkdir()
(board / LINKS_FILE).write_text("- [Booth](http://x/) <sub>· infra-ops · 2026-09-06 23:35</sub>\n"
"- [Other](http://y/) <sub>· 10.0.0.5 · 2026-09-06 23:36</sub>\n")
html = c.get("/b/links/").text
whens = [_text(w) for w in re.findall(r'<span class="board-when">(.*?)</span>', html, flags=re.S)]
assert "10.0.0.5" not in html, "a board row's author is bylined like every other"
assert len(whens) == 2 and all(HOUSE.search(w) and not HHMM.search(w) for w in whens), whens
assert "6 Sep 2335" in whens, whens
assert 'datetime="2026-09-06 23:35"' in html
def test_a_malformed_answer_costs_its_line_not_the_page(client):
"""One stored answer missing `unanswered` must not 500 the booth or marks
page (heid bug-hunt Q3). REFUTED as a live bug: the Booth's Jinja uses the
default `Undefined`, whose `|length` is 0. Kept as a guard: switching the
environment to StrictUndefined would turn this red."""
import json as _json
c, data = client
b = data / "b"
b.mkdir()
(b / "a.png").write_bytes(b"\x89PNG\r\n\x1a\n")
declare_pick(b, "p1", {"prompt": "Which?", "options": ["North", "South"]})
doc = _json.loads((b / ".marks.json").read_text())
doc["marks"][0]["answer"] = {"answers": []}
(b / ".marks.json").write_text(_json.dumps(doc))
for url in ("/b/b/", "/b/b/marks"):
assert c.get(url).status_code == 200, url
# ---- S2: legibility ----------------------------------------------------------------
# Faded is not legible; labels >= 11px (--size-micro); sentence-like lines >= 12px
# (--size-caption). These read the shipped CSS, and the arrow test composites the
# colours with INDEPENDENT maths (OKLCH -> sRGB -> WCAG), not with anything the
# page computes.
import math as _math
from pathlib import Path as _Path
_T = _Path(__file__).resolve().parent.parent / "booth" / "templates"
_EMBED = _Path(__file__).resolve().parent.parent / "booth" / "static" / "embed.js"
def _rule(css: str, selector: str) -> str:
m = re.search(r"(?:^|[}\s])" + re.escape(selector) + r"\s*\{([^}]*)\}", css)
assert m, f"no rule for {selector!r}"
return m.group(1)
def _oklch_srgb(l, c, h):
a, b = c * _math.cos(_math.radians(h)), c * _math.sin(_math.radians(h))
l_, m_, s_ = (l + 0.3963377774 * a + 0.2158037573 * b, l - 0.1055613458 * a - 0.0638541728 * b,
l - 0.0894841775 * a - 1.2914855480 * b)
L, M, S = l_ ** 3, m_ ** 3, s_ ** 3
lin = (4.0767416621 * L - 3.3077115913 * M + 0.2309699292 * S,
-1.2684380046 * L + 2.6097574011 * M - 0.3413193965 * S,
-0.0041960863 * L - 0.7034186147 * M + 1.7076147010 * S)
enc = lambda v: 12.92 * v if v <= 0.0031308 else 1.055 * v ** (1 / 2.4) - 0.055
return [max(0.0, min(1.0, enc(v))) for v in lin]
def _lum(rgb):
f = lambda v: v / 12.92 if v <= 0.03928 else ((v + 0.055) / 1.055) ** 2.4
return 0.2126 * f(rgb[0]) + 0.7152 * f(rgb[1]) + 0.0722 * f(rgb[2])
def _oklch(decl: str, prop: str):
m = re.search(prop + r"\s*:\s*oklch\(([\d.]+)\s+([\d.]+)\s+([\d.]+)(?:\s*/\s*([\d.]+))?\)", decl)
assert m, f"{prop} is not an oklch() colour in {decl!r}"
return _oklch_srgb(*map(float, m.groups()[:3])), float(m.group(4) or 1)
def test_review_arrows_hold_over_a_white_stage():
"""The worst stage is white: composite the chip over it in sRGB, as the
browser does, and measure the glyph against the result."""
decl = _rule((_T / "view.html").read_text(), ".vnav")
glyph, _ = _oklch(decl, r"(?<![-\w])color")
chip, alpha = _oklch(decl, "background")
under = [alpha * c + (1 - alpha) * 1.0 for c in chip]
a, b = _lum(glyph), _lum(under)
ratio = (max(a, b) + 0.05) / (min(a, b) + 0.05)
assert ratio >= 7, f"arrow glyph over the chip over white: {ratio:.2f}:1"
def _px(decl: str) -> float:
m = re.search(r"font(?:-size)?\s*:[^;]*?(?:(\d+(?:\.\d+)?)px|var\(--size-(micro|caption|sm|body)\))", decl)
assert m, decl
return float(m.group(1)) if m.group(1) else {"micro": 11, "caption": 12, "sm": 13, "body": 14}[m.group(2)]
def test_film_numbers_meet_the_label_floor():
assert _px(_rule((_T / "base.html").read_text(), ".film-ord")) >= 11
def test_mark_state_meets_the_label_floor():
assert _px(_rule((_T / "base.html").read_text(), ".mark-state")) >= 11
@pytest.mark.parametrize("selector", [".desk-rule", ".board-note", ".bench-note"])
def test_hint_lines_meet_the_sentence_floor(selector):
css = (_T / "base.html").read_text()
decls = [m.group(1) for m in re.finditer(r"(?:^|[}\s])" + re.escape(selector) + r"\s*\{([^}]*)\}", css)]
sizes = [_px(d) for d in decls if "font" in d]
assert sizes and min(sizes) >= 12, (selector, sizes)
def test_retired_benches_are_not_faded():
css = (_T / "base.html").read_text()
for m in re.finditer(r"([^{}]*is-retired[^{}]*)\{([^}]*)\}", css):
assert "opacity" not in m.group(2), m.group(0)
def _js_rule(js: str, selector: str) -> str:
"""embed.js carries its sheet as JS string literals: `".sel{...}"`."""
m = re.search(r'"' + re.escape(selector) + r"\{([^}]*)\}", js)
assert m, f"no rule for {selector!r} in embed.js"
return m.group(1)
def test_embed_fades_nothing():
js = _EMBED.read_text()
for sel in (".bk-ask-det", ".bk-ask-was", ".bk-ask-title"):
decl = _js_rule(js, sel)
assert "opacity" not in decl, (sel, decl)
assert not re.search(r'"\.bk-ask-was b\{', js), "a child restoring opacity means the parent fades"
ph = _js_rule(js, ".bk-ask-notes::placeholder")
assert "color:inherit" in ph.replace(" ", ""), ph
def test_the_ask_tag_meets_the_label_floor():
"""The inline ask's state tag is a label (`? your pick`, `✓ answered 28 Sep 0848`).
Its sheet is split over JS string literals, so read the font line that follows
the selector."""
js = _EMBED.read_text()
i = js.index('".bk-ask-tag{')
m = re.search(r"font:\d+ (\d+(?:\.\d+)?)px", js[i:i + 400])
assert m and float(m.group(1)) >= 11, m and m.group(0)
# ---- S6: the operator's rulings ------------------------------------------------------
def test_the_tagline_is_a_sentence(client):
c, data = client
html = c.get("/").text
m = re.search(r'<span class="tagline">(.*?)</span>', html, flags=re.S)
assert m and _text(m.group(1)) == "held for review · wipes in 24h unless kept", m and m.group(1)
# the BASE rule (the phone rule `body.page-stage .topbar .tagline` is S3's)
decl = re.search(r"\n\s*\.tagline\{([^}]*)\}", (_T / "base.html").read_text()).group(1)
assert "uppercase" not in decl and _px(decl) >= 12, decl
def test_needs_you_rows_carry_no_side_stripe():
decl = _rule((_T / "base.html").read_text(), ".desk-row.is-needs")
assert "inset" not in decl, decl
def test_the_brand_dot_is_matte():
decl = _rule((_T / "base.html").read_text(), ".brand .dot")
assert "box-shadow" not in decl, decl
# ---- S5a: accessibility plumbing (markup and CSS) --------------------------------
# Written after the code: each test is proved by its row in antislop.toml, which
# runs the change it forbids and must see it fail.
import html as _html
def _s5_booth(data):
"""A booth that renders every control the S5a claims cover: a doc, a file, an
image with a note and a flag, a titled single-question ask, and a board."""
from booth.marks import set_flag
b = data / "b"
b.mkdir()
(b / "notes.md").write_text("# Notes\n\ntext\n")
(b / "kit.zip").write_bytes(b"PK\x05\x06" + b"\x00" * 18)
(b / "a.png").write_bytes(_PNG)
(b / "c.png").write_bytes(_PNG)
set_flag(b, "a.png", True)
write_note(b, "a.png", "soft edges")
write_note(b, None, "about the booth")
declare_pick(b, "p1", {"title": "Round one", "prompt": "Which?", "options": ["North", "South"]})
# S5a fixup: a multi-question ask with per-question notes, and keys that end
# like the ids S5a derives from them (`-prompt`, `title`)
declare_pick(b, "p2", {"title": "Round two", "questions": [
{"key": "x-prompt", "prompt": "First?", "options": ["keep", "cut"], "notes": True},
{"key": "x", "prompt": "Second?", "options": ["keep", "cut"], "notes": True},
{"key": "title", "prompt": "Third?", "options": ["keep", "cut"]}]})
_s5_board(data)
return b
def _s5_board(data):
"""The link board, with a row and a bench: its controls are named too."""
from booth.benches import upsert_bench
from booth.links import LINKS_FILE
(data / "links").mkdir()
(data / "links" / LINKS_FILE).write_text("- [Booth](http://x.example/) <sub>· infra-ops · 2026-09-06 23:35</sub>\n")
upsert_bench(data, "http://10.100.10.50:5173/", "peedlar desk", "peedlar-dev")
_PNG = (b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde"
b"\x00\x00\x00\x0cIDATx\x9cc\xf8\xcf\xc0\x00\x00\x03\x01\x01\x00\xc9\xfe\x92\xef\x00\x00\x00\x00IEND\xaeB`\x82")
def _pages(c):
return {u: c.get(u).text for u in ("/", "/b/b/", "/b/b/view?f=a.png", "/b/b/compare?a=a.png&b=c.png",
"/b/b/marks", "/b/b/view?f=notes.md", "/b/links/")}
def _markup(page):
"""The page's own markup: script and style bodies are strings and comments
are prose (the stylesheet's comments name `<textarea>`), not elements."""
page = re.sub(r"<(script|style)\b.*?</\1>", " ", page, flags=re.S)
return re.sub(r"<!--.*?-->", " ", page, flags=re.S)
def _name(attrs, inner):
"""The accessible name, as far as markup decides it: aria-label wins, else
the text, counting each image's alt (an <img alt="a.png"> names its link)."""
m = re.search(r'aria-label="([^"]*)"', attrs)
if m:
return _html.unescape(m.group(1))
inner = re.sub(r'<img\b[^>]*\balt="([^"]*)"[^>]*>', r" \1 ", inner)
return _html.unescape(_text(inner))
def _controls(page):
for m in re.finditer(r"<(a|button)\b([^>]*)>(.*?)</\1>", _markup(page), flags=re.S):
if 'aria-hidden="true"' in m.group(2):
continue # a deliberate duplicate (the Desk's thumbnail link) is not a control
yield m.group(2), _name(m.group(2), m.group(3))
def test_every_control_has_a_word_for_a_name(client):
"""A control a screen reader announces as "×", "↗" or "01" has no name. Every
link and button's name, as the markup decides it, carries a word."""
c, data = client
_s5_booth(data)
for url, page in _pages(c).items():
seen = 0
for attrs, name in _controls(page):
seen += 1
assert re.search(r"[A-Za-z]", name), f"{url}: a control named {name!r}: {attrs[:120]}"
assert seen, f"{url}: no control found (positive control)"
def test_desk_row_controls_name_their_booth(client):
c, data = client
for n in ("alpha", "beta"):
(data / n).mkdir()
(data / n / "x.txt").write_text("x")
labels = re.findall(r'aria-label="(wipe the (?:kept )?booth [^"]+)"', c.get("/").text)
assert len(labels) == 2 and len(set(labels)) == 2 and any("alpha" in s for s in labels), labels
def test_fields_are_named(client):
c, data = client
_s5_booth(data)
pages = _pages(c)
pages["embed"] = " ".join(m["whole"] + m["submit"] for m in c.get("/b/b/embed.json").json()["marks"])
for url, page in pages.items():
for m in re.finditer(r"<(textarea|input)\b([^>]*)>", _markup(page), flags=re.S):
attrs = m.group(2)
if re.search(r'type="(hidden|radio|checkbox|submit)"', attrs):
continue
ok = "aria-label=" in attrs or re.search(r'id="([^"]+)"', attrs) and \
re.search(r'<label[^>]*for="%s"' % re.escape(re.search(r'id="([^"]+)"', attrs).group(1)), page)
assert ok, f"{url}: an unnamed field: {attrs[:120]}"
def test_radio_groups_are_named_and_ids_are_unique(client):
c, data = client
_s5_booth(data)
(m,) = [m for m in c.get("/b/b/embed.json").json()["marks"] if m["id"] == "p1"]
# The embed places an ask one of two ways: `whole` (title, questions and
# submit in one piece), or its questions one by one plus `submit`. Never both.
placements = {"whole": m["whole"], "parts": "".join(q["html"] for q in m["questions"]) + m["submit"]}
for how, frag in placements.items():
groups = re.findall(r'role="radiogroup" aria-labelledby="([^"]+)"', frag)
assert groups and all(f'id="{g}"' in frag for g in groups), (how, groups)
# the ASK ids are this slice's (a titled single ask emitted `bk-ask-<id>`
# twice). Mark ids (`mark-note-1`) also repeat across the tile and the
# aside; the CLI prints `#mark-<id>` links to them, so that one is
# reported, not changed here.
ids = re.findall(r'\bid="(bk-ask-[^"]+)"', frag)
dupes = {i for i in ids if ids.count(i) > 1}
assert ids and not dupes, (how, f"duplicate ask ids {sorted(dupes)[:5]}")
marks = c.get("/b/b/marks").text
for fs in re.findall(r"<fieldset\b.*?</fieldset>", marks, flags=re.S):
if 'type="radio"' in fs:
assert "<legend" in fs, fs[:160]
def test_every_page_has_one_h1_and_a_skip_link(client):
c, data = client
_s5_booth(data)
for url in ("/", "/b/b/", "/b/b/view?f=a.png", "/b/b/compare?a=a.png&b=c.png", "/b/b/marks"):
page = re.sub(r'<article class="markdown-body.*?</article>', " ", c.get(url).text, flags=re.S) # a doc's own h1 is content
assert len(re.findall(r"<h1\b", page)) == 1, (url, len(re.findall(r"<h1\b", page)))
assert re.search(r'<a class="skip-link" href="#main">', page) and 'id="main"' in page, url
def test_theme_color_for_both_schemes(client):
c, _ = client
page = c.get("/").text
for scheme in ("light", "dark"):
assert re.search(r'<meta name="theme-color" media="\(prefers-color-scheme: %s\)" content="#[0-9a-f]{6}">' % scheme, page), scheme
def test_the_tape_is_one_picture(client):
c, data = client
_s5_booth(data)
page = c.get("/b/b/view?f=a.png").text
assert re.search(r'<div class="tape" data-region="tape" role="img" aria-label="', page)
segs = re.findall(r'<a class="tape-s[^>]*>', page, flags=re.S)
assert segs and all('tabindex="-1"' in s and 'aria-hidden="true"' in s for s in segs), segs[:2]
def test_wipe_now_asks_by_name(client):
c, data = client
_s5_booth(data)
page = c.get("/b/b/").text
form = re.search(r'<form class="wipe wipe-lg"[^>]*>', page, flags=re.S).group(0)
assert 'data-booth="b"' in form and 'data-confirm="wipe"' in form and "onsubmit" not in form, form
assert "var WORDS = Object.create(null);" in page, "the shared confirm helper is on the booth page"
def test_human_dur_rolls_up_to_days():
from booth.app import human_dur
assert human_dur(47 * 3600) == "47h"
assert human_dur(48 * 3600) == "2d"
assert human_dur(167 * 3600 + 12 * 60) == "6d 23h"
def test_embed_chrome_draws_its_own_focus_rings():
js = _EMBED.read_text()
assert re.search(r'"\.booth-nav-home:focus-visible,\.booth-nav-asks:focus-visible\{outline:2px solid', js)
assert re.search(r'"\.bk-ask-go:focus-visible\{outline:2px solid', js)
assert re.search(r'"\.bk-ask-opt:has\(input:focus-visible\)\{outline:2px solid', js)
def test_focus_rings_are_drawn_inside_clipping_containers():
css = (_T / "base.html").read_text()
m = re.search(r"\.theme button:focus-visible,\.vtoggle button:focus-visible,\.item a:focus-visible,\s*\.desk-panel a:focus-visible\{outline-offset:-2px\}", css)
assert m
def test_a_truncated_why_carries_its_full_text(client):
c, data = client
b = data / "w"
b.mkdir()
(b / "x.txt").write_text("x")
(b / ".booth.json").write_text('{"handle": "design-dev", "why": "a long reason that the Desk truncates with an ellipsis"}')
page = c.get("/").text
assert re.search(r'<span class="prov-why" title="a long reason that the Desk truncates with an ellipsis">', page)
# ---- S5a fixup: booth-dev's gate (hulda bug-hunt + heid BRINGA, thread 01M3MVGQ7QSCCK8WT59TQ4J469) ----
def test_no_id_repeats_on_any_page(client):
"""An id names one element. The tile's copy of a note used to repeat the
panel article's `mark-<id>` (booth-dev: the id is the article's); a question
key ending in `-prompt`, or named `title`, repeated the ids S5a derived."""
c, data = client
_s5_booth(data)
pages = _pages(c)
for m in c.get("/b/b/embed.json").json()["marks"]:
pages[f"embed {m['id']} whole"] = m["whole"]
pages[f"embed {m['id']} parts"] = "".join(q["html"] for q in m["questions"]) + m["submit"]
for url, page in pages.items():
ids = re.findall(r'\sid="([^"]+)"', _markup(page))
dupes = sorted({i for i in ids if ids.count(i) > 1})
assert not dupes, (url, dupes[:5])
for ref in re.findall(r'aria-labelledby="([^"]+)"', page):
assert f'id="{ref}"' in page, (url, ref)
def test_release_on_the_booth_page_asks_by_name(client):
c, data = client
b = data / "k"
b.mkdir()
(b / "x.txt").write_text("x")
(b / ".forever").write_text("")
page = c.get("/b/k/").text
form = re.search(r'<form class="keep-lg"[^>]*>', page).group(0)
assert 'data-booth="k"' in form and 'data-confirm="release"' in form, form
def test_the_confirm_helper_is_listening_before_the_body_exists(client):
"""A click while the page is still loading must be asked too: the capture
listener is registered in <head>, not after the footer."""
c, data = client
_s5_booth(data)
for url in ("/", "/b/b/"):
page = c.get(url).text
assert page.index("function shown(n)") < page.index("</head>"), url
def test_human_dur_never_raises():
from booth.app import human_dur
for bad in (float("nan"), float("inf"), float("-inf")):
assert isinstance(human_dur(bad), str), bad