"""Tests for StatusBar, ChannelTree, and related widget helpers.""" import pytest from tuimble.app import ( VOLUME_STEPS, ChannelSelected, ChannelTree, StatusBar, _next_volume, ) from tuimble.client import Channel, User # -- test helpers ------------------------------------------------------------ def _sample_channels(): return { 0: Channel(channel_id=0, name="Root", parent_id=-1), 1: Channel(channel_id=1, name="Alpha", parent_id=0), 2: Channel(channel_id=2, name="Beta", parent_id=0), } def _sample_users(): return { 1: [User(session_id=10, name="Alice", channel_id=1)], } # -- A. Pure unit tests (sync) ---------------------------------------------- class TestVolBar: def test_zero(self): assert StatusBar._vol_bar(0) == "\u2591\u2591\u2591\u2591" def test_25(self): assert StatusBar._vol_bar(25) == "\u2588\u2591\u2591\u2591" def test_50(self): assert StatusBar._vol_bar(50) == "\u2588\u2588\u2591\u2591" def test_100(self): assert StatusBar._vol_bar(100) == "\u2588\u2588\u2588\u2588" def test_200_overgain(self): result = StatusBar._vol_bar(200) # 200/25 = 8, clamped to 4 filled + max(0, 4-8)=0 light assert "\u2588" in result class TestTruncate: def test_short(self): assert ChannelTree._truncate("hi", 10) == "hi" def test_exact(self): assert ChannelTree._truncate("abcde", 5) == "abcde" def test_long(self): assert ChannelTree._truncate("abcdefghij", 7) == "abcd..." def test_tiny_max(self): assert ChannelTree._truncate("abcdef", 3) == "abc" def test_max_two(self): assert ChannelTree._truncate("abcdef", 2) == "ab" def test_zero_max(self): assert ChannelTree._truncate("abc", 0) == "" class TestUserStatus: def test_normal(self): u = User(session_id=1, name="X", channel_id=0) assert ChannelTree._user_status(u) == "" def test_self_deaf(self): u = User(session_id=1, name="X", channel_id=0, self_deaf=True) assert "\u2298" in ChannelTree._user_status(u) def test_self_mute(self): u = User(session_id=1, name="X", channel_id=0, self_mute=True) assert "\u2715" in ChannelTree._user_status(u) def test_server_deaf(self): u = User(session_id=1, name="X", channel_id=0, deaf=True) assert "\u2298" in ChannelTree._user_status(u) def test_server_mute(self): u = User(session_id=1, name="X", channel_id=0, mute=True) assert "\u2715" in ChannelTree._user_status(u) def test_deaf_takes_priority(self): u = User(session_id=1, name="X", channel_id=0, self_deaf=True, self_mute=True) assert "\u2298" in ChannelTree._user_status(u) class TestNextVolume: def test_full_cycle(self): vol = 0.0 seen = [vol] for _ in range(len(VOLUME_STEPS)): vol = _next_volume(vol) seen.append(vol) # Must cycle through all steps and wrap assert seen[-1] == VOLUME_STEPS[0] def test_wraparound_from_max(self): assert _next_volume(VOLUME_STEPS[-1]) == VOLUME_STEPS[0] def test_mid_value(self): assert _next_volume(0.5) == 0.75 # -- B. ChannelTree state tests (sync, direct instantiation) ---------------- class TestChannelTreeState: def test_build_order_matches_dfs(self): tree = ChannelTree() tree.set_state(_sample_channels(), {}) # Root, then sorted children: Alpha, Beta assert tree._channel_ids == [0, 1, 2] def test_empty_channels(self): tree = ChannelTree() tree.set_state({}, {}) assert tree._channel_ids == [] def test_clear_state(self): tree = ChannelTree() tree.set_state(_sample_channels(), _sample_users(), my_channel_id=1) tree.clear_state() assert tree._channels == {} assert tree._users_by_channel == {} assert tree._channel_ids == [] assert tree._focused_idx == 0 assert tree._my_channel_id is None def test_focus_clamped_when_shrinks(self): tree = ChannelTree() tree.set_state(_sample_channels(), {}) tree._focused_idx = 2 # last channel # Shrink to only root tree.set_state({0: Channel(channel_id=0, name="Root", parent_id=-1)}, {}) assert tree._focused_idx == 0 def test_focus_preserved_when_in_range(self): tree = ChannelTree() tree.set_state(_sample_channels(), {}) tree._focused_idx = 1 # Re-set same state tree.set_state(_sample_channels(), {}) assert tree._focused_idx == 1 def test_nested_channels_order(self): channels = { 0: Channel(channel_id=0, name="Root", parent_id=-1), 1: Channel(channel_id=1, name="Zulu", parent_id=0), 2: Channel(channel_id=2, name="Alpha", parent_id=0), 3: Channel(channel_id=3, name="Sub", parent_id=2), } tree = ChannelTree() tree.set_state(channels, {}) # Root -> Alpha (sorted) -> Sub -> Zulu assert tree._channel_ids == [0, 2, 3, 1] # -- C. StatusBar integration tests (async) --------------------------------- from textual.app import App, ComposeResult # noqa: E402 class StatusBarApp(App): CSS = """ #status { dock: bottom; height: 1; } """ def compose(self) -> ComposeResult: yield StatusBar(id="status") @pytest.mark.asyncio async def test_statusbar_default_disconnected(): app = StatusBarApp() async with app.run_test(size=(80, 5)) as _pilot: bar = app.query_one("#status", StatusBar) assert bar.connected is False rendered = bar.render() assert "\u25cb" in rendered # disconnected circle @pytest.mark.asyncio async def test_statusbar_connected(): app = StatusBarApp() async with app.run_test(size=(80, 5)) as _pilot: bar = app.query_one("#status", StatusBar) bar.connected = True rendered = bar.render() assert "\u25cf" in rendered # filled circle assert "connected" in rendered @pytest.mark.asyncio async def test_statusbar_compact_width(): app = StatusBarApp() async with app.run_test(size=(30, 5)) as pilot: bar = app.query_one("#status", StatusBar) await pilot.resize_terminal(30, 5) await pilot.pause() rendered = bar.render() # Compact mode: symbols only, no labels assert "connected" not in rendered @pytest.mark.asyncio async def test_statusbar_medium_width(): app = StatusBarApp() async with app.run_test(size=(50, 5)) as _pilot: bar = app.query_one("#status", StatusBar) rendered = bar.render() assert "disconnected" in rendered @pytest.mark.asyncio async def test_statusbar_full_width(): app = StatusBarApp() async with app.run_test(size=(80, 5)) as _pilot: bar = app.query_one("#status", StatusBar) bar.connected = True bar.output_vol = 100 bar.input_vol = 50 rendered = bar.render() assert "out" in rendered assert "in" in rendered @pytest.mark.asyncio async def test_statusbar_ptt_active(): app = StatusBarApp() async with app.run_test(size=(80, 5)) as _pilot: bar = app.query_one("#status", StatusBar) bar.ptt_active = True rendered = bar.render() assert "TX" in rendered @pytest.mark.asyncio async def test_statusbar_deaf(): app = StatusBarApp() async with app.run_test(size=(80, 5)) as _pilot: bar = app.query_one("#status", StatusBar) bar.self_deaf = True rendered = bar.render() assert "\u2298" in rendered @pytest.mark.asyncio async def test_statusbar_reconnecting(): app = StatusBarApp() async with app.run_test(size=(80, 5)) as _pilot: bar = app.query_one("#status", StatusBar) bar.reconnecting = True rendered = bar.render() assert "reconnecting" in rendered # -- D. ChannelTree integration tests (async) -------------------------------- class ChannelTreeApp(App): CSS = """ #sidebar { width: 24; height: 1fr; } """ BINDINGS = [("q", "quit", "Quit")] def __init__(self): super().__init__() self.selected_messages: list[int] = [] def compose(self) -> ComposeResult: yield ChannelTree(id="sidebar") def on_channel_selected(self, msg: ChannelSelected) -> None: self.selected_messages.append(msg.channel_id) @pytest.mark.asyncio async def test_tree_empty_render(): app = ChannelTreeApp() async with app.run_test(size=(40, 10)) as _pilot: tree = app.query_one("#sidebar", ChannelTree) rendered = tree.render() assert "(not connected)" in rendered @pytest.mark.asyncio async def test_tree_populated(): app = ChannelTreeApp() async with app.run_test(size=(40, 10)) as _pilot: tree = app.query_one("#sidebar", ChannelTree) tree.set_state(_sample_channels(), _sample_users()) rendered = tree.render() assert "Root" in rendered assert "Alpha" in rendered assert "Beta" in rendered assert "Alice" in rendered @pytest.mark.asyncio async def test_tree_down_arrow(): app = ChannelTreeApp() async with app.run_test(size=(40, 10)) as pilot: tree = app.query_one("#sidebar", ChannelTree) tree.set_state(_sample_channels(), {}) tree.focus() await pilot.pause() assert tree._focused_idx == 0 await pilot.press("down") assert tree._focused_idx == 1 @pytest.mark.asyncio async def test_tree_up_arrow(): app = ChannelTreeApp() async with app.run_test(size=(40, 10)) as pilot: tree = app.query_one("#sidebar", ChannelTree) tree.set_state(_sample_channels(), {}) tree._focused_idx = 2 tree.focus() await pilot.pause() await pilot.press("up") assert tree._focused_idx == 1 @pytest.mark.asyncio async def test_tree_bounds_top(): app = ChannelTreeApp() async with app.run_test(size=(40, 10)) as pilot: tree = app.query_one("#sidebar", ChannelTree) tree.set_state(_sample_channels(), {}) tree.focus() await pilot.pause() await pilot.press("up") assert tree._focused_idx == 0 # stays at 0 @pytest.mark.asyncio async def test_tree_bounds_bottom(): app = ChannelTreeApp() async with app.run_test(size=(40, 10)) as pilot: tree = app.query_one("#sidebar", ChannelTree) tree.set_state(_sample_channels(), {}) tree._focused_idx = 2 tree.focus() await pilot.pause() await pilot.press("down") assert tree._focused_idx == 2 # stays at max @pytest.mark.asyncio async def test_tree_enter_posts_message(): app = ChannelTreeApp() async with app.run_test(size=(40, 10)) as pilot: tree = app.query_one("#sidebar", ChannelTree) tree.set_state(_sample_channels(), {}) tree._focused_idx = 1 # Alpha (channel_id=1) tree.focus() await pilot.pause() await pilot.press("enter") await pilot.pause() assert 1 in app.selected_messages