"""Tests for dotfiles/hyprland/.local/bin/waybar-airplane. The script emits one JSON line for waybar's custom/airplane module, derived from the airplane-mode state file that the airplane-mode toggle maintains at $XDG_RUNTIME_DIR/airplane-state. The file holds key=value lines; the only key the indicator reads is `mode` (on/off). Tests point XDG_RUNTIME_DIR at a temp dir and assert on the emitted JSON. No mocking: the real script runs against a real state file. Run from repo root: python3 -m unittest tests.waybar-airplane.test_waybar_airplane """ import json import os import shutil import subprocess import tempfile import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) SCRIPT = os.path.join(REPO_ROOT, "dotfiles/hyprland/.local/bin/waybar-airplane") class WaybarAirplaneHarness(unittest.TestCase): def setUp(self): self.tmp = tempfile.mkdtemp(prefix="waybar-airplane-test-") self.state_file = os.path.join(self.tmp, "airplane-state") # Fake power-supply dir with a battery → the script treats the host as # a laptop. Desktop tests point this at a battery-less dir. self.ps_dir = os.path.join(self.tmp, "power_supply") os.makedirs(os.path.join(self.ps_dir, "BAT0")) def tearDown(self): shutil.rmtree(self.tmp, ignore_errors=True) def set_state(self, contents): with open(self.state_file, "w") as f: f.write(contents) def run_script(self): env = os.environ.copy() env["XDG_RUNTIME_DIR"] = self.tmp env["AIRPLANE_POWER_SUPPLY_DIR"] = self.ps_dir return subprocess.run( [SCRIPT], env=env, capture_output=True, text=True, timeout=10, ) def emitted_json(self): result = self.run_script() self.assertEqual(result.returncode, 0, msg=result.stderr) return json.loads(result.stdout) # ----------------------------------------------------------------------------- # Normal cases # ----------------------------------------------------------------------------- class TestWaybarAirplaneNormal(WaybarAirplaneHarness): def test_mode_on_emits_active_class_and_plane_icon(self): # State is carried by class/color, not a slashed glyph: the same clear # plane shows in both states (gold when active, gray when inactive). self.set_state("mode=on\nwifi=enabled\n") data = self.emitted_json() self.assertEqual(data["class"], "active") self.assertIn("", data["text"]) # plane self.assertIn("on", data["tooltip"].lower()) def test_mode_off_emits_inactive_class_and_plane_icon(self): self.set_state("mode=off\n") data = self.emitted_json() self.assertEqual(data["class"], "inactive") self.assertIn("", data["text"]) # plane self.assertIn("off", data["tooltip"].lower()) # ----------------------------------------------------------------------------- # Boundary cases # ----------------------------------------------------------------------------- class TestWaybarAirplaneBoundary(WaybarAirplaneHarness): def test_missing_state_file_defaults_to_inactive(self): # No state file → airplane mode is not engaged (radios on = safe default). data = self.emitted_json() self.assertEqual(data["class"], "inactive") def test_mode_on_with_other_keys_present(self): # The toggle writes several keys; the indicator only cares about mode. self.set_state( "mode=on\nwifi=enabled\nepp=balance_performance\n" "brightness=96000\nstopped_system=sshd.service tailscaled.service\n" ) data = self.emitted_json() self.assertEqual(data["class"], "active") def test_unknown_mode_value_treated_as_inactive(self): # Anything that isn't "on" reads as off (fail-safe: radios shown as on). self.set_state("mode=garbage\n") data = self.emitted_json() self.assertEqual(data["class"], "inactive") def test_empty_state_file_defaults_to_inactive(self): self.set_state("") data = self.emitted_json() self.assertEqual(data["class"], "inactive") def test_output_is_a_single_json_object(self): self.set_state("mode=on\n") result = self.run_script() lines = [ln for ln in result.stdout.splitlines() if ln.strip()] self.assertEqual(len(lines), 1, msg=f"expected one line, got {lines!r}") # ----------------------------------------------------------------------------- # Laptop gating — the module only shows on machines with a battery # ----------------------------------------------------------------------------- class TestWaybarAirplaneLaptopGating(WaybarAirplaneHarness): def run_desktop(self): # Point the power-supply dir at a battery-less location (a desktop). env = os.environ.copy() env["XDG_RUNTIME_DIR"] = self.tmp empty = os.path.join(self.tmp, "power_supply_desktop") os.makedirs(empty, exist_ok=True) # AC adapter only, no BAT* os.makedirs(os.path.join(empty, "ADP1"), exist_ok=True) env["AIRPLANE_POWER_SUPPLY_DIR"] = empty return subprocess.run( [SCRIPT], env=env, capture_output=True, text=True, timeout=10, ) def test_desktop_emits_nothing(self): # No battery → module hidden → script prints no output. self.set_state("mode=off\n") result = self.run_desktop() self.assertEqual(result.returncode, 0, msg=result.stderr) self.assertEqual(result.stdout.strip(), "") def test_desktop_emits_nothing_even_when_engaged(self): self.set_state("mode=on\n") result = self.run_desktop() self.assertEqual(result.stdout.strip(), "") def test_laptop_with_battery_emits_module(self): # Sanity: the battery-present harness (default) does emit JSON. self.set_state("mode=off\n") data = self.emitted_json() self.assertEqual(data["class"], "inactive") if __name__ == "__main__": unittest.main()