Each test docstring states the behaviour that the test proves, in the simple present. No assertion message, test id, or other string literal changed. Four statements now agree with the code: the Trace highlight stays on the last row, a menu count, and two others. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
167 lines
7.3 KiB
Python
167 lines
7.3 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""Header tests: the activity sparkline that fills the width of the one-line status bar."""
|
|
|
|
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 substitute that has only what the header reads (the simulator path).
|
|
|
|
``battery`` is the cached reading of the poller (an object with ``percent`` and
|
|
``charging``), or ``None`` for a companion with no pack. ``None`` is the default, so
|
|
most tests see the old header with no battery.
|
|
"""
|
|
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:
|
|
"""Alerts that are not acknowledged show as the triangle badge. With none, there is 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:
|
|
"""The styles of a segment are side by side. They are never in layers, one under the next.
|
|
|
|
The code builds the segments as separate ``Text`` objects and joins them (refer to
|
|
:func:`~meshterm.ui.menu._header_segments`). It is easy to use ``style=`` in the
|
|
constructor, but then that style becomes the base style of the object. The base style
|
|
covers all the text that the code appends after it. Then the version string would
|
|
render as brand under muted, and the count of each badge would get the error red of
|
|
its glyph.
|
|
"""
|
|
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", "●", "▲"} # the count of each badge is not in it
|
|
|
|
|
|
def test_header_sparkline_fills_the_row_exactly() -> None:
|
|
"""The sparkline fills the row exactly.
|
|
|
|
Each cell that the fixed segments leave is sparkline. There are no short rows and no
|
|
overflow.
|
|
"""
|
|
for width in (60, 100):
|
|
header = _header(_ctx(histogram=(21,) * 360), {}, width)
|
|
assert header.cell_len == width
|
|
assert "⣿" in header.plain # cells at the maximum, to the edge
|
|
|
|
|
|
def test_header_puts_now_at_the_right_edge() -> None:
|
|
"""The header puts now at the right edge.
|
|
|
|
The traffic of the current minute is drawn in the last cell of the row, and not in the
|
|
first cell.
|
|
"""
|
|
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 right dot column of the last cell. Its left column is the
|
|
# minute before, which is silent, except for the baseline dot that continues.
|
|
assert spark[-1] == chr(0x2800 | 0xB8 | 0x40)
|
|
assert all(ch == chr(0x2800 | 0x40 | 0x80) for ch in spark[:-1]) # the older cells are flat
|
|
|
|
|
|
def test_header_wider_terminal_shows_deeper_history() -> None:
|
|
"""A wider terminal shows a deeper history.
|
|
|
|
A burst 90 minutes ago is not in a narrow header, but it is visible in a wide header.
|
|
One dot column is one minute. For 90 minutes back, the sparkline needs 45 cells. Thus
|
|
the burst shows as a lit cell only when the row has that much room.
|
|
"""
|
|
histogram = tuple(0 if i != 90 else 21 for i in range(360))
|
|
|
|
def burst_cells(row: str) -> list[str]:
|
|
# Each dot that is lit above the bottom dot row (bits other than dots 7 and 8)
|
|
# is the burst. The flat baseline 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 to the peak of the window.
|
|
|
|
The pulse scales relative to a peak, and not with a fixed ladder of thresholds. A ramp
|
|
of 16, 12, 8, 4 climbs one dot for each step against the robust ceiling that
|
|
``activity_peak`` computes for it. Here the ceiling is approximately 14.8, which is a
|
|
90th percentile of the counts, with a floor. Thus the four newest minutes show the
|
|
heights 1, 2, 3, 4 from left to right, in the last two cells of the row.
|
|
"""
|
|
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:
|
|
"""A tight row changes to compact separators.
|
|
|
|
When wide separators would make the pulse too small, they become narrow (' · ').
|
|
"""
|
|
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 compact separators give real room to the sparkline, also on a row of 60 columns
|
|
# with many badges.
|
|
spark = [ch for ch in tight if 0x2800 <= ord(ch) <= 0x28FF]
|
|
assert len(spark) >= 14
|
|
|
|
|
|
def test_header_survives_a_sliver_terminal() -> None:
|
|
"""The header survives a terminal that is only a sliver.
|
|
|
|
When the fixed segments already overflow the width, the sparkline is not drawn.
|
|
"""
|
|
header = _header(_ctx(histogram=(9,) * 360), {}, 10)
|
|
# There is no room left, so there is no braille at all. Nothing goes 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:
|
|
"""The header pins the battery gauge to the right edge.
|
|
|
|
A companion with a pack shows its charge in the corner. The pulse gives the room to it.
|
|
"""
|
|
reading = SimpleNamespace(percent=87, charging=False)
|
|
header = _header(_ctx(histogram=(21,) * 360, battery=reading), {}, 80)
|
|
assert header.cell_len == 80 # the header still fills the row exactly, with the gauge
|
|
assert header.plain.rstrip().endswith("87%") # the gauge is flush right
|
|
# The pulse gave real cells for the room. It has fewer sparkline cells than a bare row,
|
|
# also when the count includes the full-cell glyph of the gauge.
|
|
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:
|
|
"""If there is no pack, there is no gauge. The pulse keeps the whole row that it always had."""
|
|
header = _header(_ctx(histogram=(21,) * 360, battery=None), {}, 80)
|
|
assert "%" not in header.plain
|
|
assert header.cell_len == 80
|