aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_layout_measured.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-08 22:54:16 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-08 22:54:16 +0200
commit1a495f5ed3f8a55222d406152c833882264dc377 (patch)
tree0c048544cba200fe5ba939d45edd311b2fc69e59 /packages/meshbay-hub/tests/test_layout_measured.py
parent6803447a8a5cc7a612d08bb858394fd7ae1b049c (diff)
downloadmeshbay-1a495f5ed3f8a55222d406152c833882264dc377.tar.gz
feat(hub): client-side transfer leases and the transfers panel
Steps 5 and 6 of ~/next/improve-downloads.md. The node has handed out slots since step 2 and nothing asked for one; now the client does, and the panel shows what is happening. `transport.openTransfer()` returns a Lease: `acquire()` resolves when the node grants, `release()` gives it back exactly once, and nothing else in the client speaks to the node about slots. Whether a node hands out slots is read from the handshake ack rather than guessed from a timeout — "no answer yet" and "this node will never answer" are indistinguishable in time, and guessing wrong either stalls every download or defeats the cap. Two things exist only because a queue can lie: a watchdog re-asks when a pushed grant does not arrive (the node is idempotent on `tr`, so asking again is free), and a grant for a transfer the page has forgotten is handed straight back rather than held until the node's deadline. The slot is asked for **after** there is somewhere to write, and that ordering is load-bearing: opening a target takes thirty seconds of streamed-download timeouts, or as long as somebody leaves a Save As dialog open, and a grant not taken up in time is revoked. Moving it earlier looked better and broke three downloads into one. Pinned by a test. The panel groups by state — running, waiting, finished — rather than re-sorting a flat list, so a row moves only when its own state does. The ETA is withheld until the speed window holds real measurement: a figure from the first two chunks swings between four seconds and an hour, and people plan around the first number they see. One live region announces state changes and not progress. Three silent paths closed on the way: a download refused for want of a user gesture (a browser grants one file picker per gesture, and downloading three files is one gesture) now falls back to the streamed path, which needs none; a click with no connection says so instead of doing nothing at all; and a queued transfer counts as busy, so a transport is never closed under one that is waiting for a grant that could then never arrive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub/tests/test_layout_measured.py')
-rw-r--r--packages/meshbay-hub/tests/test_layout_measured.py119
1 files changed, 118 insertions, 1 deletions
diff --git a/packages/meshbay-hub/tests/test_layout_measured.py b/packages/meshbay-hub/tests/test_layout_measured.py
index 91f1ed0..a71b6b9 100644
--- a/packages/meshbay-hub/tests/test_layout_measured.py
+++ b/packages/meshbay-hub/tests/test_layout_measured.py
@@ -54,7 +54,7 @@ NAV = textwrap.dedent("""
<div class="transfer-item">
<div class="transfer-line">
<span class="transfer-kind">&#8595;</span>
- <span class="transfer-name">S03E01. Salt and Sea, Fire and Blood.mp4</span>
+ <span class="transfer-name">Some Saga S03E01 - A Long Enough Title.mp4</span>
<button class="transfer-cancel">&#10005;</button>
</div>
<div class="dl-progress"><div class="dl-fill" style="width:42%"></div></div>
@@ -144,3 +144,120 @@ def test_the_page_does_not_scroll_sideways(measured, width):
r = measured[str(width)]
assert r["docScrollW"] <= r["viewport"]["w"], (
f"the document scrolls to {r['docScrollW']} px on a {width} px screen")
+
+
+# ── The grouped panel (§8.2) ────────────────────────────────────────────────
+#
+# The panel gained groups, a header summary and a waiting row. Every one of
+# those can push something off a 320 px screen, and none of it can be seen by
+# reading the stylesheet: what decides where the panel lands is the button it
+# hangs from, which is not at the right edge. That is the defect this file was
+# written for, and it comes back with any change to the header's width.
+
+GROUPED = textwrap.dedent("""
+ <nav class="nav">
+ <div class="nav-left">
+ <button class="nav-hamburger">&#9776;</button>
+ <a class="nav-brand" href="#/">MeshBay</a>
+ </div>
+ <div class="nav-right">
+ <div class="transfer-wrap">
+ <button class="nav-notif transfer-btn">&#8595;</button>
+ <div class="transfer-panel">
+ <div class="transfer-head">
+ <span class="transfer-head-title">Transfers</span>
+ <span class="transfer-head-summary">2 running &middot; 3 waiting</span>
+ <button class="btn-secondary">Clear finished</button>
+ </div>
+ <div class="transfer-group">
+ <div class="transfer-group-head">Running</div>
+ <div class="transfer-item transfer-running">
+ <div class="transfer-line">
+ <span class="transfer-kind">&#8595;</span>
+ <span class="transfer-name">Some Saga S03E01 - A Long Enough Title.mp4</span>
+ <button class="transfer-cancel">&#10005;</button>
+ </div>
+ <div class="dl-progress"><div class="dl-fill" style="width:42%"></div></div>
+ <div class="transfer-meta"><span>210 MB / 493 MB</span><span>3.1 MB/s &middot; 4 min left</span></div>
+ </div>
+ </div>
+ <div class="transfer-group">
+ <div class="transfer-group-head">Waiting</div>
+ <div class="transfer-item transfer-queued">
+ <div class="transfer-line">
+ <span class="transfer-kind">&#8595;</span>
+ <span class="transfer-name">Another File With A Long Name.mkv</span>
+ <button class="transfer-cancel">&#10005;</button>
+ </div>
+ <div class="dl-progress dl-waiting"></div>
+ <div class="transfer-meta"><span>Waiting &mdash; your slots are busy</span><span>1.2 GB</span></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <a class="nav-notif" href="#/">&#128276;</a>
+ <div class="user-menu"><button class="nav-btn">someone</button></div>
+ </div>
+ </nav>
+""")
+
+GROUPED_SELECTORS = [".transfer-panel", ".transfer-head", ".transfer-head-summary",
+ ".transfer-group-head", ".transfer-name",
+ ".transfer-item.transfer-queued .dl-progress"]
+
+
+@pytest.fixture(scope="module")
+def grouped(tmp_path_factory):
+ fragment = tmp_path_factory.mktemp("grouped") / "fragment.html"
+ fragment.write_text(GROUPED)
+ proc = subprocess.run(
+ ["python3", str(HARNESS), ",".join(str(w) for w in WIDTHS),
+ str(fragment), *GROUPED_SELECTORS],
+ capture_output=True, text=True, timeout=180)
+ assert proc.returncode == 0, f"probe failed: {proc.stdout}{proc.stderr}"
+ out = json.loads(proc.stdout)
+ assert "error" not in out, f"no measurement: {out}"
+ return out
+
+
+def test_the_grouped_panel_stays_on_a_phone_screen(grouped):
+ for width in WIDTHS:
+ box = grouped[str(width)]["boxes"][".transfer-panel"]
+ assert box["offLeft"] == 0, (
+ f"at {width} px the panel hangs {box['offLeft']} px off the left — "
+ "which is where the file names are")
+ assert box["offRight"] == 0, (
+ f"at {width} px the panel hangs {box['offRight']} px off the right")
+
+
+def test_the_file_name_is_on_screen_in_every_group(grouped):
+ for width in WIDTHS:
+ box = grouped[str(width)]["boxes"][".transfer-name"]
+ assert box["offLeft"] == 0 and box["offRight"] == 0, (
+ f"at {width} px a file name is cut off: {box}")
+ assert box["width"] > 40, "the name column collapsed to nothing"
+
+
+def test_the_header_summary_does_not_push_the_header_taller(grouped):
+ """It is the one part of the header that grows with what is happening. If
+ it wraps, the header changes height as transfers come and go and every row
+ below it moves — on the narrowest screen, repeatedly."""
+ for width in WIDTHS:
+ head = grouped[str(width)]["boxes"][".transfer-head"]
+ summary = grouped[str(width)]["boxes"][".transfer-head-summary"]
+ assert head["height"] <= 48, (
+ f"at {width} px the header is {head['height']} px tall — it wrapped")
+ assert summary["height"] <= 24, (
+ f"at {width} px the summary wrapped to {summary['height']} px")
+
+
+def test_the_waiting_bar_is_as_wide_as_a_progress_bar(grouped):
+ """A waiting row has no inner fill element — the stripes are on the track
+ itself. Getting that wrong renders a zero-width bar, which reads as a
+ transfer stuck at 0% rather than one that has not started."""
+ for width in WIDTHS:
+ bar = grouped[str(width)]["boxes"][
+ ".transfer-item.transfer-queued .dl-progress"]
+ assert bar["width"] > 100, (
+ f"at {width} px the waiting bar is {bar['width']} px wide")
+ assert bar["height"] >= 3, "the waiting bar has no height"