`ruff format .` across the repo: 153 files, no behaviour change. This is the second gate CONTRIBUTING.md promises, and until now it reported 154 files needing reformat — so a first contributor ran the command the guide told them to run and could not tell their own diff from the background. Landed on its own, with nothing else in it, because a whole-repo reformat is only safe to skip past in blame if it is provably nothing but layout. It was checked that way rather than assumed: every changed file was formatted in memory and compared by full `ast.dump()`, and 152 of 153 come out structurally identical (the one exception was a docstring opening on a quote character, fixed in the commit before this one). The string and comment token streams were diffed separately, so a quote flip or an edited comment could not hide inside a re-layout — five quote-style normalisations and sixteen implicit-concatenation joins, all value-identical, and one commented-out line gaining the space after its `#`. `meshterm specimen` is byte-identical on both platforms, and the suite is 1888 green, gallery gate and `test_theme16` included. What actually changed, in order of how much of the diff it is: wrapped calls and signatures rejoin onto one line where they fit inside the hundred columns; the magic trailing comma expands what it is there to expand; complex slices gain their spaces (`[len(noise) :]`); a few redundant parens go. The tables that are laid out to be read were fenced in the previous commit and are untouched here. The aligned trailing-comment column is gone from about ninety lanes that were not fenced — including the `heat.*` ladder in `MESH_THEME`, which cannot be fenced where it sits. Every comment stays on the entry it describes, so nothing there reads wrong; it just no longer lines up. Add this commit to `.git-blame-ignore-revs`, which follows immediately. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
144 lines
6.5 KiB
Python
144 lines
6.5 KiB
Python
"""Header tests: the one-line status bar's width-filling activity sparkline."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from meshterm.ui.menu import _header
|
|
|
|
|
|
def _ctx(histogram=(), unread=0, alerts=0, battery=None) -> SimpleNamespace:
|
|
"""A minimal stand-in exposing only what the header reads (simulator path).
|
|
|
|
``battery`` is the poller's cached reading (a ``percent``/``charging`` object) or
|
|
``None`` for a companion with no pack — the default, so most tests see the old
|
|
battery-free header.
|
|
"""
|
|
return SimpleNamespace(
|
|
mock=True,
|
|
chat=SimpleNamespace(unread_total=lambda: unread),
|
|
monitor=SimpleNamespace(
|
|
activity_histogram=lambda: tuple(histogram),
|
|
activity_session_flags=lambda: (True,) * len(tuple(histogram)),
|
|
),
|
|
watchtower=SimpleNamespace(unacked_count=lambda: alerts),
|
|
battery=SimpleNamespace(reading=lambda: battery),
|
|
)
|
|
|
|
|
|
def test_header_shows_the_watchtower_badge() -> None:
|
|
"""Unacknowledged alerts appear as the triangle badge; none means no badge."""
|
|
assert "▲ 2" in _header(_ctx(alerts=2), {}, 80).plain
|
|
assert "▲" not in _header(_ctx(alerts=0), {}, 80).plain
|
|
|
|
|
|
def test_header_segments_style_each_run_on_its_own() -> None:
|
|
"""A segment's styles sit side by side, never layered one under the next.
|
|
|
|
The segments are built as their own ``Text`` objects and joined (see
|
|
:func:`~meshterm.ui.menu._header_segments`), which makes it easy to reach for a
|
|
constructor ``style=`` — but that becomes the object's *base* style and blankets
|
|
everything appended after it, so the version string would render brand-under-muted and
|
|
each badge's count would inherit its glyph's error red.
|
|
"""
|
|
header = _header(_ctx(unread=3, alerts=2), {}, 120)
|
|
covering = {
|
|
header.plain[span.start : span.end]: span.style
|
|
for span in header.spans
|
|
if span.style in ("brand", "err")
|
|
}
|
|
assert covering.get("MeshTerm") == "brand" # not "MeshTerm v1.2.3"
|
|
assert set(covering) == {"MeshTerm", "●", "▲"} # neither badge's count is swept in
|
|
|
|
|
|
def test_header_sparkline_fills_the_row_exactly() -> None:
|
|
"""Every cell the fixed segments leave is sparkline — no short rows, no overflow."""
|
|
for width in (60, 100):
|
|
header = _header(_ctx(histogram=(21,) * 360), {}, width)
|
|
assert header.cell_len == width
|
|
assert "⣿" in header.plain # maxed-out cells all the way to the edge
|
|
|
|
|
|
def test_header_puts_now_at_the_right_edge() -> None:
|
|
"""The current minute's traffic draws in the row's final cell, not its first."""
|
|
header = _header(_ctx(histogram=(21,) + (0,) * 359), {}, 80).plain
|
|
spark = [ch for ch in header if 0x2800 <= ord(ch) <= 0x28FF]
|
|
# The newest bucket is the final cell's *right* dot column (its left column is
|
|
# the minute before, silent bar the continuing baseline dot).
|
|
assert spark[-1] == chr(0x2800 | 0xB8 | 0x40)
|
|
assert all(ch == chr(0x2800 | 0x40 | 0x80) for ch in spark[:-1]) # older = flatline
|
|
|
|
|
|
def test_header_wider_terminal_shows_deeper_history() -> None:
|
|
"""A burst 90 minutes ago is off a narrow header but visible on a wide one.
|
|
|
|
One minute per dot column: 90 minutes back needs 45 cells of sparkline, so it
|
|
renders as a lit cell only when the row leaves that much room.
|
|
"""
|
|
histogram = tuple(0 if i != 90 else 21 for i in range(360))
|
|
|
|
def burst_cells(row: str) -> list[str]:
|
|
# Anything lit above the bottom dot row (bits beyond dots 7/8) is the burst;
|
|
# the baseline flatline and blank braille never set those bits.
|
|
return [ch for ch in row if 0x2800 <= ord(ch) <= 0x28FF and ord(ch) & 0x3F]
|
|
|
|
assert not burst_cells(_header(_ctx(histogram=histogram), {}, 60).plain)
|
|
assert burst_cells(_header(_ctx(histogram=histogram), {}, 120).plain)
|
|
|
|
|
|
def test_header_scales_to_the_window_peak() -> None:
|
|
"""The pulse scales relative to a peak, not a fixed threshold ladder.
|
|
|
|
A 16/12/8/4 ramp climbs one dot per step against the robust ceiling ``activity_peak``
|
|
computes for it (~14.8 here — a floored 90th percentile of the counts), so the four
|
|
newest minutes read heights 1,2,3,4 left→right in the row's final two cells.
|
|
"""
|
|
histogram = (16, 12, 8, 4) + (0,) * 356
|
|
header = _header(_ctx(histogram=histogram), {}, 80).plain
|
|
spark = [ch for ch in header if 0x2800 <= ord(ch) <= 0x28FF]
|
|
left = (0x00, 0x40, 0x44, 0x46, 0x47)
|
|
right = (0x00, 0x80, 0xA0, 0xB0, 0xB8)
|
|
assert spark[-2:] == [chr(0x2800 | left[1] | right[2]), chr(0x2800 | left[3] | right[4])]
|
|
|
|
|
|
def test_header_tight_row_drops_to_compact_separators() -> None:
|
|
"""When roomy separators would squeeze the pulse, they narrow to ' · '."""
|
|
roomy = _header(_ctx(), {}, 100).plain
|
|
assert " · " in roomy
|
|
tight = _header(_ctx(unread=12, alerts=3), {}, 60).plain
|
|
assert " · " not in tight and " · " in tight
|
|
# The compaction buys real sparkline room even on a badge-laden 60-column row.
|
|
spark = [ch for ch in tight if 0x2800 <= ord(ch) <= 0x28FF]
|
|
assert len(spark) >= 14
|
|
|
|
|
|
def test_header_survives_a_sliver_terminal() -> None:
|
|
"""When the fixed segments already overflow the width, the sparkline just stays out."""
|
|
header = _header(_ctx(histogram=(9,) * 360), {}, 10)
|
|
# No room left: no braille at all, nothing stretched past the edge.
|
|
assert all(not (0x2800 <= ord(ch) <= 0x28FF) for ch in header.plain)
|
|
|
|
|
|
def _spark(row: str) -> list[str]:
|
|
return [ch for ch in row if 0x2800 <= ord(ch) <= 0x28FF]
|
|
|
|
|
|
def test_header_pins_the_battery_gauge_to_the_right_edge() -> None:
|
|
"""A companion with a pack shows its charge in the corner; the pulse cedes the room."""
|
|
reading = SimpleNamespace(percent=87, charging=False)
|
|
header = _header(_ctx(histogram=(21,) * 360, battery=reading), {}, 80)
|
|
assert header.cell_len == 80 # still fills the row exactly, gauge included
|
|
assert header.plain.rstrip().endswith("87%") # the gauge sits flush right
|
|
# The pulse gave up real cells to make room (fewer sparkline cells than a bare row,
|
|
# even counting the gauge's own full-cell glyph).
|
|
with_batt = len(_spark(header.plain))
|
|
bare = len(_spark(_header(_ctx(histogram=(21,) * 360), {}, 80).plain))
|
|
assert with_batt < bare
|
|
|
|
|
|
def test_header_hides_the_gauge_without_a_battery() -> None:
|
|
"""No pack, no gauge — the pulse keeps the whole row it always had."""
|
|
header = _header(_ctx(histogram=(21,) * 360, battery=None), {}, 80)
|
|
assert "%" not in header.plain
|
|
assert header.cell_len == 80
|