From the anti-slop run (design-dev, 2026-09-28). A rendered doc ran 110-120 characters a line and its h3 sat at 1.08x its body. Prose blocks in .markdown-body are capped at 72ch (pre and tables keep the full width, where they scroll), and h3/h2/h1 step at 1.2/1.44/1.73em. Measured in a real browser: a long paragraph now reads under 76 characters across, and every heading step is >= 1.18. Contract: as_antislop S4. Falsifiers: antislop.toml 43/43 proved (S1-S4); all 12 tables 323/323 proved on this tree.
157 lines
6.9 KiB
Python
157 lines
6.9 KiB
Python
"""S3 of the anti-slop fix slices, measured in a real browser at phone width.
|
|
|
|
A layout claim read off a stylesheet is a guess, so these open the pages in the
|
|
same real uvicorn + Chromium harness as test_flow_browser and measure boxes. At
|
|
390x844 no text may overprint other text, and no word may be set in a column
|
|
narrower than itself. Skips, never fails, when no usable Chromium exists.
|
|
"""
|
|
|
|
import pathlib
|
|
import sys
|
|
|
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
|
from test_flow_browser import _png, browser, live # noqa: E402,F401 (fixtures)
|
|
|
|
PHONE = {"width": 390, "height": 844}
|
|
|
|
|
|
def _boxes_intersect(a, b, slack=1.0):
|
|
return (min(a["r"], b["r"]) - max(a["l"], b["l"]) > slack
|
|
and min(a["b"], b["b"]) - max(a["t"], b["t"]) > slack)
|
|
|
|
|
|
_RECT = "e => { const b = e.getBoundingClientRect(); return {l: b.left, r: b.right, t: b.top, b: b.bottom, w: b.width, h: b.height}; }"
|
|
|
|
|
|
def _board(root: pathlib.Path):
|
|
from booth.benches import set_bench_state, upsert_bench
|
|
from booth.links import LINKS_FILE
|
|
b = root / "links"
|
|
b.mkdir()
|
|
(b / LINKS_FILE).write_text(
|
|
"- [talk, a fleet voice (HTTPS, trusted cert)](https://talk.example:8092/) <sub>· tts-dev · 2026-09-06 23:35</sub>\n"
|
|
"- [a second link](http://x.example/) <sub>· infra-ops · 2026-09-07 09:12</sub>\n")
|
|
for url, name, owner, state in [("http://10.100.10.50:5173/", "peedlar desk", "peedlar-dev", "live"),
|
|
("http://10.100.10.50:8931/board", "svos board", "highseat-dev", "promoted"),
|
|
("http://10.100.10.50:7788/talk", "talk demo", "booth", "retired")]:
|
|
bench, _ = upsert_bench(root, url, name, owner)
|
|
set_bench_state(root, bench.id, state)
|
|
return b
|
|
|
|
|
|
def test_bench_rows_do_not_overprint_on_a_phone(browser, live):
|
|
base, root = live
|
|
_board(root)
|
|
page = browser.new_page(viewport=PHONE)
|
|
page.goto(f"{base}/b/links/", wait_until="load")
|
|
rows = page.locator(".bench-row")
|
|
assert rows.count() == 3, "positive control: the three benches render"
|
|
for i in range(rows.count()):
|
|
row = rows.nth(i)
|
|
parts = {sel: row.locator(sel).first.evaluate(_RECT)
|
|
for sel in (".bench-state", ".bench-link", ".bench-url", ".bench-who", ".bench-when", ".bench-acts")}
|
|
names = list(parts)
|
|
for x in range(len(names)):
|
|
for y in range(x + 1, len(names)):
|
|
assert not _boxes_intersect(parts[names[x]], parts[names[y]]), \
|
|
f"row {i}: {names[x]} overprints {names[y]}: {parts[names[x]]} / {parts[names[y]]}"
|
|
main = row.locator(".bench-main").evaluate(_RECT)
|
|
assert main["w"] >= 200, f"row {i}: the name's column is squeezed to {main['w']:.0f}px"
|
|
page.close()
|
|
|
|
|
|
def test_board_head_stays_compact_on_a_phone(browser, live):
|
|
base, root = live
|
|
_board(root)
|
|
page = browser.new_page(viewport=PHONE)
|
|
page.goto(f"{base}/b/links/", wait_until="load")
|
|
for sel in (".board-title", ".bench-title"):
|
|
title = page.locator(sel).evaluate(
|
|
"e => ({h: e.getBoundingClientRect().height, lh: parseFloat(getComputedStyle(e).lineHeight) || parseFloat(getComputedStyle(e).fontSize) * 1.3})")
|
|
assert title["h"] <= title["lh"] * 1.5, f"{sel} wraps: {title}"
|
|
page.close()
|
|
|
|
|
|
def test_doc_name_keeps_a_readable_line_on_a_phone(browser, live):
|
|
base, root = live
|
|
d = root / "d"
|
|
d.mkdir()
|
|
(d / "01-information-architecture.md").write_text("# The Booth\n\nA paragraph of text.\n")
|
|
page = browser.new_page(viewport=PHONE)
|
|
page.goto(f"{base}/b/d/", wait_until="load")
|
|
name = page.locator(".doc-name").first.evaluate(_RECT)
|
|
assert name["w"] >= 240, f"the doc name is set {name['w']:.0f}px wide"
|
|
page.close()
|
|
|
|
|
|
def test_file_tile_number_clears_the_download_link(browser, live):
|
|
base, root = live
|
|
f = root / "f"
|
|
f.mkdir()
|
|
(f / "ldp-demo-kit-2026-09-23-18b8cee.zip").write_bytes(b"PK\x05\x06" + b"\x00" * 18)
|
|
for vp in (PHONE, {"width": 1280, "height": 800}):
|
|
page = browser.new_page(viewport=vp)
|
|
page.goto(f"{base}/b/f/", wait_until="load")
|
|
ord_box = page.locator(".item > .ord").first.evaluate(_RECT)
|
|
text_box = page.locator(".item .dl").first.evaluate(
|
|
"e => { const r = document.createRange(); r.selectNodeContents(e); const b = r.getBoundingClientRect();"
|
|
" return {l: b.left, r: b.right, t: b.top, b: b.bottom, w: b.width, h: b.height}; }")
|
|
assert not _boxes_intersect(ord_box, text_box), (vp, ord_box, text_box)
|
|
page.close()
|
|
|
|
|
|
def test_review_header_is_one_line_on_a_phone(browser, live):
|
|
base, root = live
|
|
g = root / "g"
|
|
g.mkdir()
|
|
(g / "a.png").write_bytes(_png(64, 48))
|
|
(g / "b.png").write_bytes(_png(64, 48))
|
|
page = browser.new_page(viewport=PHONE)
|
|
for url in (f"{base}/b/g/view?f=a.png", f"{base}/b/g/compare?a=a.png&b=b.png"):
|
|
page.goto(url, wait_until="load")
|
|
assert page.locator(".topbar .tagline").evaluate("e => getComputedStyle(e).display") == "none", url
|
|
assert page.locator(".topbar").evaluate("e => e.getBoundingClientRect().height") <= 64, url
|
|
page.goto(f"{base}/", wait_until="load")
|
|
assert page.locator(".topbar .tagline").evaluate("e => getComputedStyle(e).display") != "none", \
|
|
"the Desk keeps its tagline (negative control)"
|
|
page.close()
|
|
|
|
|
|
# ---- S4: reading measure --------------------------------------------------------
|
|
|
|
_DOC = "# The Booth\n\n## What it is\n\n### Stated once\n\n" + ("The Booth is the fleet's operator-review surface. Agents post work; the operator looks at it, judges it, and the judgment gets back to the agent. " * 4) + "\n"
|
|
|
|
|
|
def _doc_page(browser, live):
|
|
base, root = live
|
|
d = root / "d"
|
|
d.mkdir()
|
|
(d / "doc.md").write_text(_DOC)
|
|
page = browser.new_page(viewport={"width": 1280, "height": 900})
|
|
page.goto(f"{base}/b/d/view?f=doc.md", wait_until="load")
|
|
return page
|
|
|
|
|
|
def test_doc_prose_reads_at_a_book_measure(browser, live):
|
|
page = _doc_page(browser, live)
|
|
chars = page.locator(".markdown-body p").first.evaluate("""p => {
|
|
const probe = document.createElement('span');
|
|
probe.textContent = '0'.repeat(100);
|
|
probe.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap';
|
|
p.appendChild(probe);
|
|
const ch = probe.getBoundingClientRect().width / 100;
|
|
probe.remove();
|
|
return p.getBoundingClientRect().width / ch;
|
|
}""")
|
|
assert chars <= 76, f"a paragraph runs {chars:.0f} characters across"
|
|
page.close()
|
|
|
|
|
|
def test_doc_headings_step_by_size(browser, live):
|
|
page = _doc_page(browser, live)
|
|
size = lambda sel: page.locator(f".markdown-body {sel}").first.evaluate("e => parseFloat(getComputedStyle(e).fontSize)")
|
|
body, h3, h2, h1 = size("p"), size("h3"), size("h2"), size("h1")
|
|
for lo, hi, name in ((body, h3, "h3:body"), (h3, h2, "h2:h3"), (h2, h1, "h1:h2")):
|
|
assert hi / lo >= 1.18, f"{name} is {hi / lo:.2f} ({hi}px over {lo}px)"
|
|
page.close()
|