Files
MeshTerm/tests/test_widgets.py
T
Jean-Pierre MartineauandClaude Opus 4.8 115fe6895d Dialogs: multi-layer floats, red data-loss tier, unknown-relay hashes
Three independent consistency passes:

1. **Message paths: unknown relays as grey full-hash**
   The message-paths dialog showed unnamed relay hops as a single lit byte (e.g.
   `e8`). Now they render at the device's path-hash-mode width, muted grey, and
   annotated with their addressed byte — `e839f2 (e8)` — so the rows stay
   readable and cross-reference the route graph's byte labels (matching named
   hops' trace presentation). Added `hash_as_name` flag to path_text widget.

2. **Dialogs: multi-layer compositor + backdrop for empty stack**
   Opening a confirm over a floating popup used to stretch that popup to
   full-frame (the single-float compositor's bug). Now the compositor maintains
   a pool of _MAX_DIALOG_LAYERS=8 centered-box floats, each rendering one layer
   above the background — so a popup and its confirm both stay their own size.
   Also: button_dialog/typed_confirm/autocomplete now float over a blank base
   when the stack is empty (via _run_dialog_screen), guaranteeing popups render
   as small boxes on an empty stack, not full-frame chrome. Simplified
   message_dialog to delegate its backdrop logic to button_dialog.

3. **Data-loss dialogs: red `destructive` tier**
   Added a second caution tier to the dialog API: danger=True stays amber
   (disruptive: discard edits, reboot), destructive=True is red (irreversible
   data loss). Updated: single-record delete, channel "clear slot", courier
   "clear finished" (new confirm), and startup "remove device" (splash confirm).
   Per UX spec: bulk/irreversible deletions gate behind typed_confirm (also red);
   single-record delete is a red Cancel/[verb] button dialog.

All changes are backward-compatible. Full suite: 793 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 23:30:58 -04:00

104 lines
4.5 KiB
Python

"""Shared-widget helper tests: the canonical formatting primitives every screen leans on.
Most widgets are exercised through their host screens' tests; what lives here are the
pure text helpers whose exact output *is* the app-wide convention — get these right once
and every caller inherits it.
"""
from __future__ import annotations
from meshterm.ui.widgets import _format_age, format_ago, path_text
def test_format_age_is_the_bare_column_form() -> None:
"""The lane form stays suffix-free at every magnitude, for aligned age columns."""
assert _format_age(None) == "never"
assert _format_age(5) == "now"
assert _format_age(90) == "1m"
assert _format_age(7200) == "2h"
assert _format_age(180000) == "2d"
assert _format_age(1300000) == "2w"
def test_format_ago_speaks_grammatical_prose() -> None:
"""The prose form says "5m ago" but bare "now"/"never" — never "now ago"."""
assert format_ago(5) == "now"
assert format_ago(None) == "never"
assert format_ago(90) == "1m ago"
assert format_ago(7200) == "2h ago"
def _resolve(hop: str) -> str:
return {"aa": "Alice", "3d": "YUL"}.get(hop, hop)
def test_path_text_names_hops_and_keeps_hashes_bare() -> None:
"""Named hops read as their name alone; unnamed ones as their bare hash."""
text = path_text(["aa", "77", "3d"], _resolve)
assert text.plain == "Alice → 77 → YUL" # no parenthesized hash after a name
def test_path_text_marks_us_white_and_names_in_their_hue() -> None:
"""Our own node takes the white ``you`` style; other names their palette hue."""
text = path_text(["aa", "3d"], _resolve, self_name="Alice")
styles = {text.plain[s.start : s.end]: str(s.style) for s in text.spans}
assert styles.get("Alice") == "you"
assert "YUL" in styles and styles["YUL"] != "you"
def test_path_text_empty_reads_as_direct() -> None:
"""No hops (or only empty tokens) renders the caller's empty word, muted."""
assert path_text([], _resolve).plain == "direct"
assert path_text(["", ""], _resolve).plain == "direct"
assert path_text([], _resolve, empty="direct — no relays").plain == "direct — no relays"
def test_path_text_lights_the_hash_prefix() -> None:
"""An unnamed hop renders through the shared hash widget, its prefix lit."""
text = path_text(["77bb"], _resolve, prefix_bytes=1)
brand = [text.plain[s.start : s.end] for s in text.spans if "brand" in str(s.style)]
assert "77" in brand # the addressed prefix stands out within the hash
def test_path_text_hash_as_name_shows_grey_identity_hash() -> None:
"""An unnamed hop stands in its own hash at the mode width, muted, plus its byte."""
text = path_text(
["e839f2ab"], _resolve, prefix_bytes=3,
show_hash=True, hash_bytes=1, hash_as_name=True,
)
assert text.plain == "e839f2 (e8)" # mode width identity, then the addressed byte
styles = {text.plain[s.start : s.end]: str(s.style) for s in text.spans}
assert styles.get("e839f2") == "muted" # greyed — colour is the "this is a name" cue
assert not any(str(s.style) == "brand" for s in text.spans) # no prefix lit
def test_path_text_hash_as_name_drops_the_byte_when_it_is_the_whole_hash() -> None:
"""When the mode width is one byte, the identity already is the byte — no ``(e8)``."""
text = path_text(
["e839f2ab"], _resolve, prefix_bytes=1,
show_hash=True, hash_bytes=1, hash_as_name=True,
)
assert text.plain == "e8"
def test_path_text_trace_flavour_annotates_hashes_and_brackets_us() -> None:
"""show_hash appends the addressed hash to names; None hops are our device."""
text = path_text(
[None, "aa", "77bb", None], _resolve, self_name="Homestead",
show_hash=True, hash_bytes=1, device_hash="c0ffee",
)
assert text.plain == "Homestead (c0) → Alice (aa) → 77 → Homestead (c0)"
def test_path_text_dims_the_tail_from_dim_from() -> None:
"""The mirrored return leg (and the arrows into it) render faint."""
text = path_text(
[None, "aa", "3d", "aa", None], _resolve, self_name="us",
show_hash=True, dim_from=3,
)
styles = [(text.plain[s.start : s.end], str(s.style)) for s in text.spans]
assert ("us", "you") in styles # the departure keeps the white you
assert ("us", "faint") in styles # the landing back on us is faint
assert any(run.startswith("Alice") and style == "faint" for run, style in styles)
assert any(run.startswith("YUL") and style != "faint" for run, style in styles)