"""Screen layout logic — resolving `petctl jump` targets and describing the setup. Pure: the monitor list is normally published by the UI, so none of this needs a display.""" import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from bolt_pet import monitors as m def grid(): """The 2x2 setup this was built against: four 1080p screens. [1 HDMI-0] [2 HDMI-1] [3 DP-0 ] [4 DP-2 ] """ return [ m.Monitor(0, "HDMI-0", 0, 0, 1920, 1080, primary=False), m.Monitor(1, "HDMI-1", 1920, 0, 1920, 1080, primary=False), m.Monitor(2, "DP-0", 0, 1080, 1920, 1080, primary=True), m.Monitor(3, "DP-2", 1920, 1080, 1920, 1080, primary=False), ] def two(): return [ m.Monitor(0, "eDP-1", 0, 0, 1920, 1080, primary=True), m.Monitor(1, "HDMI-1", 1920, 0, 2560, 1440), ] def test_numbers_shown_to_humans_are_one_based(): left, right = two() assert left.index == 0 and left.number == 1 assert right.index == 1 and right.number == 2 assert "1: eDP-1 1920x1080 (primary)" == left.label def test_geometry_helpers(): screen = m.Monitor(1, "HDMI-1", 1920, 0, 1920, 1080) assert screen.right == 3840 and screen.bottom == 1080 assert screen.center == (2880, 540) assert screen.contains(1920, 0) assert screen.contains(3839, 1079) assert not screen.contains(3840, 0) # right edge is exclusive assert not screen.contains(1919, 0) def test_monitor_containing_and_nearest(): screens = grid() assert m.monitor_containing(screens, 100, 100).name == "HDMI-0" assert m.monitor_containing(screens, 2000, 1500).name == "DP-2" assert m.monitor_containing(screens, -50, -50) is None # off the desktop entirely still resolves to something assert m.nearest_monitor(screens, -500, -500).name == "HDMI-0" def test_resolve_by_number(): screens = grid() assert m.resolve(screens, "3").name == "DP-0" with pytest.raises(ValueError, match="no monitor 9"): m.resolve(screens, "9") with pytest.raises(ValueError): m.resolve(screens, "0") def test_resolve_next_and_prev_wrap(): screens = grid() assert m.resolve(screens, "next", current=3).number == 1 assert m.resolve(screens, "prev", current=0).number == 4 assert m.resolve(screens, "next", current=0).number == 2 def test_resolve_primary_and_other(): screens = grid() assert m.resolve(screens, "primary", current=0).name == "DP-0" # "other" on a two-screen setup is genuinely the other one pair = two() assert m.resolve(pair, "other", current=0).number == 2 assert m.resolve(pair, "other", current=1).number == 1 def test_resolve_directions_on_a_grid(): screens = grid() # from top-left (HDMI-0) assert m.resolve(screens, "right", current=0).name == "HDMI-1" assert m.resolve(screens, "down", current=0).name == "DP-0" # from bottom-right (DP-2) assert m.resolve(screens, "left", current=3).name == "DP-0" assert m.resolve(screens, "up", current=3).name == "HDMI-1" def test_direction_prefers_the_best_aligned_screen(): screens = grid() # "right" from DP-0 (bottom-left) must pick DP-2 (same row), not HDMI-1, # even though both are to the right. assert m.resolve(screens, "right", current=2).name == "DP-2" def test_resolve_direction_with_nothing_there(): screens = grid() with pytest.raises(ValueError, match="no monitor to the left"): m.resolve(screens, "left", current=0) def test_resolve_by_name_is_fuzzy_but_refuses_ambiguity(): screens = grid() assert m.resolve(screens, "dp-2").name == "DP-2" assert m.resolve(screens, "HDMI-0").name == "HDMI-0" with pytest.raises(ValueError, match="matches several"): m.resolve(screens, "hdmi") def test_resolve_unknown_spec_lists_the_options(): screens = two() with pytest.raises(ValueError) as excinfo: m.resolve(screens, "the big one") assert "eDP-1" in str(excinfo.value) and "HDMI-1" in str(excinfo.value) def test_resolve_without_a_current_screen_falls_back_to_primary(): screens = grid() # primary is index 2, so "next" from nowhere is index 3 assert m.resolve(screens, "next", current=None).number == 4 # an out-of-range current is treated the same way rather than exploding assert m.resolve(screens, "next", current=99).number == 4 def test_resolve_needs_monitors(): with pytest.raises(ValueError, match="no monitors"): m.resolve([], "next") with pytest.raises(ValueError, match="needs a target"): m.resolve(grid(), "") def test_random_always_moves_somewhere_else(): screens = grid() for current in range(4): assert m.resolve(screens, "random", current=current).index != current def test_summary_is_one_line_and_marks_where_the_pet_is(): line = m.summary(grid(), current=1) assert "\n" not in line assert line.startswith("4 monitors:") assert "Bolt is on 2" in line assert m.summary([]) is None assert "Bolt is on" not in m.summary(grid(), current=None) def test_annotate_matches_the_screen_context_style(): out = m.annotate("what's on the other screen?", grid(), current=0) assert out.startswith("what's on the other screen?") assert "[4 monitors:" in out # nothing to say, nothing added assert m.annotate("hello", [], None) == "hello" assert m.annotate("", grid(), 0) == "" def test_describe_lists_every_screen_and_flags_the_pet(): text = m.describe(grid(), current=2) assert text.count("\n") == 4 # header + 4 screens assert "DP-0" in text and "+0+1080" in text assert text.count("Bolt is here") == 1 assert m.describe([]) == "[pet] no monitor information available"