use super::*; use crate::launcher::{ devices::{CameraMode, DeviceCatalog}, preview::PreviewBinding, state::{BreakoutSizePreset, LauncherState, PreviewSourceSize}, ui_components::build_launcher_view, }; use gtk::prelude::*; use serial_test::serial; use std::{ cell::RefCell, collections::BTreeMap, rc::Rc, time::{Duration, Instant}, }; fn present_and_settle(window: >k::ApplicationWindow) { window.set_default_size(1280, 780); window.present(); let deadline = Instant::now() + Duration::from_millis(450); while Instant::now() < deadline { while glib::MainContext::default().iteration(false) {} std::thread::sleep(Duration::from_millis(15)); } } #[test] fn local_test_detail_mentions_idle_and_running_modes() { assert!(local_test_detail(false, false, false, false).contains("idle")); let running = local_test_detail(true, true, false, false); assert!(running.contains("camera preview")); assert!(running.contains("mic monitor")); } #[test] fn gpio_power_label_tracks_detected_devices() { let mut power = CapturePowerStatus::default(); assert_eq!(gpio_power_label(&power), "Unavailable"); power.available = true; assert_eq!(gpio_power_label(&power), "Power Off"); power.enabled = true; assert_eq!(gpio_power_label(&power), "No Eyes"); power.detected_devices = 1; assert_eq!(gpio_power_label(&power), "1 Eye"); power.detected_devices = 2; assert_eq!(gpio_power_label(&power), "2 Eyes"); } #[gtk::test] #[serial] fn launcher_shell_measures_inside_a_1080p_desktop_budget() { if gtk::gdk::Display::default().is_none() { return; } let app = gtk::Application::builder() .application_id("dev.lesavka.test-layout-budget") .build(); let _ = app.register(None::<>k::gio::Cancellable>); let state = LauncherState::new(); let view = build_launcher_view( &app, "http://127.0.0.1:50051", &DeviceCatalog::default(), &state, ); present_and_settle(&view.window); let (min_width, natural_width, _, _) = view.window.measure(gtk::Orientation::Horizontal, -1); let (min_height, natural_height, _, _) = view.window.measure(gtk::Orientation::Vertical, 1920); assert!( min_width <= 1280 && view.window.width() <= 1280, "launcher width budget regressed: min={min_width}, natural={natural_width}" ); assert!( min_height <= 900 && view.window.height() <= 900 && natural_height <= 1080, "launcher height budget regressed: min={min_height}, natural={natural_height}" ); } #[gtk::test] #[serial] fn populated_launcher_shell_measures_inside_a_1080p_desktop_budget() { if gtk::gdk::Display::default().is_none() { return; } let app = gtk::Application::builder() .application_id("dev.lesavka.test-layout-budget-populated") .build(); let _ = app.register(None::<>k::gio::Cancellable>); let catalog = realistic_device_catalog(); let mut state = LauncherState::new(); state.apply_catalog_defaults(&catalog); let view = build_launcher_view(&app, "http://127.0.0.1:50051", &catalog, &state); present_and_settle(&view.window); let (min_width, natural_width, _, _) = view.window.measure(gtk::Orientation::Horizontal, -1); let (min_height, natural_height, _, _) = view.window.measure(gtk::Orientation::Vertical, 1920); assert!( min_width <= 1280 && view.window.width() <= 1280, "populated launcher width budget regressed: min={min_width}, natural={natural_width}" ); assert!( min_height <= 900 && view.window.height() <= 900 && natural_height <= 1080, "populated launcher height budget regressed: min={min_height}, natural={natural_height}" ); } #[gtk::test] #[serial] fn populated_launcher_runtime_widgets_stay_compact() { if gtk::gdk::Display::default().is_none() { return; } let app = gtk::Application::builder() .application_id("dev.lesavka.test-layout-widget-budget") .build(); let _ = app.register(None::<>k::gio::Cancellable>); let catalog = realistic_device_catalog(); let mut state = LauncherState::new(); state.apply_catalog_defaults(&catalog); let view = build_launcher_view(&app, "http://127.0.0.1:50051", &catalog, &state); present_and_settle(&view.window); let (camera_min_w, camera_nat_w, _, _) = view .device_stage .camera_preview .measure(gtk::Orientation::Horizontal, -1); let (camera_min_h, camera_nat_h, _, _) = view .device_stage .camera_preview .measure(gtk::Orientation::Vertical, 160); let (testing_panel_min_h, testing_panel_nat_h, _, _) = view .device_stage .preview_panel .measure(gtk::Orientation::Vertical, 320); let (left_min_w, left_nat_w, _, _) = view.widgets.display_panes[0] .root .measure(gtk::Orientation::Horizontal, -1); let (left_min_h, left_nat_h, _, _) = view.widgets.display_panes[0] .root .measure(gtk::Orientation::Vertical, 640); let (server_min_w, server_nat_w, _, _) = view.server_entry.measure(gtk::Orientation::Horizontal, -1); assert!( camera_min_w <= 160 && camera_nat_w <= 160, "camera preview width regressed: min={camera_min_w}, natural={camera_nat_w}" ); assert!( camera_min_h <= 90 && camera_nat_h <= 90, "camera preview height regressed: min={camera_min_h}, natural={camera_nat_h}" ); assert!( testing_panel_min_h <= 260 && testing_panel_nat_h <= 260, "device testing panel height regressed: min={testing_panel_min_h}, natural={testing_panel_nat_h}" ); assert!( left_min_w <= 445 && left_nat_w <= 470, "eye pane width regressed: min={left_min_w}, natural={left_nat_w}" ); assert!( left_min_h <= 520 && left_nat_h <= 520, "eye pane height regressed: min={left_min_h}, natural={left_nat_h}" ); assert!( server_min_w <= 168 && server_nat_w <= 180, "server entry width regressed: min={server_min_w}, natural={server_nat_w}" ); assert!( (view.device_stage.devices_panel.height() - view.device_stage.preview_panel.height()).abs() <= 2, "device staging and upstream media heights diverged: staging={}, upstream={}", view.device_stage.devices_panel.height(), view.device_stage.preview_panel.height() ); assert!( view.widgets.display_panes[0].preview_frame.height() >= view.device_stage.camera_preview_frame.height(), "eye preview should stay at least as tall as the webcam preview: eye={}, webcam={}", view.widgets.display_panes[0].preview_frame.height(), view.device_stage.camera_preview_frame.height() ); } #[gtk::test] #[serial] fn breakout_size_changes_resize_the_open_popout_window() { if gtk::gdk::Display::default().is_none() { return; } let app = gtk::Application::builder() .application_id("dev.lesavka.test-breakout-resize") .build(); let _ = app.register(None::<>k::gio::Cancellable>); let window = gtk::ApplicationWindow::builder() .application(&app) .default_width(540) .default_height(304) .build(); let root = gtk::Box::new(gtk::Orientation::Vertical, 0); let frame = gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false); let picture = gtk::Picture::new(); frame.set_child(Some(&picture)); root.append(&frame); window.set_child(Some(&root)); let handle = PopoutWindowHandle { window, root, frame: frame.clone(), picture: picture.clone(), status_label: gtk::Label::new(None), binding: PreviewBinding::test_stub(), }; apply_popout_window_size( &handle, BreakoutSizeChoice { preset: BreakoutSizePreset::P720, width: 1280, height: 720, }, PreviewSourceSize { width: 1920, height: 1080, fps: 60, }, ); assert_eq!(handle.root.width_request(), 1280); assert_eq!(handle.root.height_request(), 720); assert_eq!(handle.frame.width_request(), 1280); assert_eq!(handle.frame.height_request(), 720); assert_eq!(handle.picture.width_request(), 1280); assert_eq!(handle.picture.height_request(), 720); } #[test] fn server_chip_state_tracks_connection_not_just_reachability() { let mut state = LauncherState::new(); assert_eq!(server_light_state(&state, false), StatusLightState::Idle); assert_eq!(server_version_label(&state), "-"); state.set_server_available(true); state.set_server_version(Some("0.12.3".to_string())); assert_eq!(server_light_state(&state, false), StatusLightState::Live); assert_eq!(server_version_label(&state), "v0.12.3"); assert_eq!( server_light_state(&state, true), StatusLightState::Connected ); state.set_server_version(Some("v0.12.4".to_string())); assert_eq!(server_light_state(&state, false), StatusLightState::Warning); assert_eq!(server_light_state(&state, true), StatusLightState::Caution); assert_eq!(server_version_label(&state), "v0.12.4"); state.set_server_version(Some(" ".to_string())); assert_eq!(server_light_state(&state, false), StatusLightState::Idle); assert_eq!(server_version_label(&state), "-"); } #[test] fn capture_power_detail_mentions_detected_eyes_when_powered() { let power = CapturePowerStatus { available: true, enabled: true, detail: "active/running".to_string(), detected_devices: 1, ..Default::default() }; assert!(capture_power_detail(&power).contains("1 eye detected")); } #[test] fn compact_device_name_prefers_basename_when_available() { assert_eq!(compact_device_name("/dev/video0"), "video0"); assert_eq!(compact_device_name("alsa_input.usb"), "alsa_input.usb"); } #[test] fn strip_ansi_sequences_removes_terminal_codes() { let raw = "\u{1b}[32mINFO\u{1b}[0m hello"; assert_eq!(strip_ansi_sequences(raw), "INFO hello"); } #[test] fn classify_log_tags_assigns_prefix_and_severity_colors() { let tags = classify_log_tags("[relay] WARN pipeline failed"); assert!(tags.contains(&"log-relay")); assert!(tags.contains(&"log-error") || tags.contains(&"log-warn")); } #[test] #[doc = "Verifies the default console filter hides relay INFO noise."] fn session_log_filter_hides_noisy_info_by_default_but_keeps_errors() { assert!(!should_show_session_log_line( "[relay] 2026-04-22T23:20:17Z INFO ThreadId(01) audio packet received packet=3000", ConsoleLogLevel::Warn )); assert!(!should_show_session_log_line( "[relay] 2026-04-22T23:20:17Z INFO ThreadId(04) decoded audio level rms=-32", ConsoleLogLevel::Warn )); assert!(should_show_session_log_line( "[relay] 2026-04-22T23:20:17Z WARN pipeline is recovering", ConsoleLogLevel::Warn )); assert!(should_show_session_log_line( "[relay] 2026-04-22T23:20:17Z INFO ❌ connect failed", ConsoleLogLevel::Error )); assert!(should_show_session_log_line( "[launcher] Relay connected with inputs routed to remote.", ConsoleLogLevel::Error )); assert!(should_show_session_log_line( "[relay] 2026-04-22T23:20:17Z INFO audio packet received", ConsoleLogLevel::Info )); } #[test] fn write_audio_gain_request_formats_live_control_file() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("gain.control"); write_audio_gain_request(&path, 425).expect("write gain"); let raw = std::fs::read_to_string(path).expect("read gain"); assert!(raw.starts_with("4.250 "), "{raw}"); } #[test] fn write_mic_gain_request_formats_live_control_file() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("mic-gain.control"); write_mic_gain_request(&path, 325).expect("write gain"); let raw = std::fs::read_to_string(path).expect("read gain"); assert!(raw.starts_with("3.250 "), "{raw}"); } #[gtk::test] #[serial] fn dock_all_displays_to_preview_closes_popouts_and_resets_surfaces() { if gtk::gdk::Display::default().is_none() { return; } let app = gtk::Application::builder() .application_id("dev.lesavka.test-dock") .build(); let _ = app.register(None::<>k::gio::Cancellable>); let state = Rc::new(RefCell::new(LauncherState::new())); state .borrow_mut() .set_display_surface(0, DisplaySurface::Window); state .borrow_mut() .set_display_surface(1, DisplaySurface::Window); let state_snapshot = state.borrow().clone(); let view = build_launcher_view( &app, "http://127.0.0.1:50051", &DeviceCatalog::default(), &state_snapshot, ); let child_proc = Rc::new(RefCell::new(None::)); let left_binding = PreviewBinding::test_stub(); let right_binding = PreviewBinding::test_stub(); { let mut popouts = view.popouts.borrow_mut(); popouts[0] = Some(PopoutWindowHandle { window: gtk::ApplicationWindow::builder() .application(&app) .title("Left") .build(), root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), binding: left_binding, }); popouts[1] = Some(PopoutWindowHandle { window: gtk::ApplicationWindow::builder() .application(&app) .title("Right") .build(), root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), binding: right_binding, }); } dock_all_displays_to_preview(&state, &child_proc, &view.popouts, &view.widgets); assert!(view.popouts.borrow().iter().all(|handle| handle.is_none())); assert_eq!(state.borrow().display_surface(0), DisplaySurface::Preview); assert_eq!(state.borrow().display_surface(1), DisplaySurface::Preview); } #[gtk::test] #[serial] fn dock_all_displays_to_preview_handles_reentrant_close_callbacks() { if gtk::gdk::Display::default().is_none() { return; } let app = gtk::Application::builder() .application_id("dev.lesavka.test-reentrant-dock") .build(); let _ = app.register(None::<>k::gio::Cancellable>); let state = Rc::new(RefCell::new(LauncherState::new())); state .borrow_mut() .set_display_surface(0, DisplaySurface::Window); let state_snapshot = state.borrow().clone(); let view = build_launcher_view( &app, "http://127.0.0.1:50051", &DeviceCatalog::default(), &state_snapshot, ); let child_proc = Rc::new(RefCell::new(None::)); let popouts = Rc::clone(&view.popouts); let window = gtk::ApplicationWindow::builder() .application(&app) .title("Reentrant") .build(); { let popouts = Rc::clone(&popouts); window.connect_close_request(move |_| { let _ = popouts.borrow_mut()[0].take(); glib::Propagation::Proceed }); } { let mut slot = popouts.borrow_mut(); slot[0] = Some(PopoutWindowHandle { window, root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), binding: PreviewBinding::test_stub(), }); } dock_all_displays_to_preview(&state, &child_proc, &popouts, &view.widgets); assert!(popouts.borrow().iter().all(|handle| handle.is_none())); assert_eq!(state.borrow().display_surface(0), DisplaySurface::Preview); } fn realistic_device_catalog() -> DeviceCatalog { DeviceCatalog { cameras: vec!["usb-046d_Logitech_BRIO_5F6EB379-video-index0".to_string()], camera_modes: [( "usb-046d_Logitech_BRIO_5F6EB379-video-index0".to_string(), vec![ CameraMode::new(1920, 1080, 30), CameraMode::new(1280, 720, 30), ], )] .into_iter() .collect::>(), microphones: vec![ "alsa_input.usb-Focusrite_Scarlett_2i2_USB_Y7ABC12345-00.analog-stereo".to_string(), ], speakers: vec![ "alsa_output.pci-0000_00_1f.3.analog-stereo".to_string(), "bluez_output.80_C3_BA_76_26_AB.1".to_string(), ], keyboards: vec!["usb-Corsair_K70_RGB_PRO_Mechanical_Gaming_Keyboard-event-kbd".to_string()], mice: vec!["usb-Logitech_G502_X_LIGHTSPEED_Gaming_Mouse-event-mouse".to_string()], } } #[gtk::test] #[serial] fn shutdown_launcher_runtime_closes_preview_bindings_and_popouts() { if gtk::gdk::Display::default().is_none() { return; } let app = gtk::Application::builder() .application_id("dev.lesavka.test-shutdown") .build(); let _ = app.register(None::<>k::gio::Cancellable>); let state = Rc::new(RefCell::new(LauncherState::new())); let state_snapshot = state.borrow().clone(); let view = build_launcher_view( &app, "http://127.0.0.1:50051", &DeviceCatalog::default(), &state_snapshot, ); let child_proc = Rc::new(RefCell::new(None::)); let tests = Rc::new(RefCell::new(DeviceTestController::new())); let left_binding = PreviewBinding::test_stub(); let right_binding = PreviewBinding::test_stub(); *view.widgets.display_panes[0].preview_binding.borrow_mut() = Some(left_binding.clone()); *view.widgets.display_panes[1].preview_binding.borrow_mut() = Some(right_binding.clone()); { let mut popouts = view.popouts.borrow_mut(); popouts[0] = Some(PopoutWindowHandle { window: gtk::ApplicationWindow::builder() .application(&app) .title("Left") .build(), root: gtk::Box::new(gtk::Orientation::Vertical, 0), frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false), picture: gtk::Picture::new(), status_label: gtk::Label::new(None), binding: PreviewBinding::test_stub(), }); } *view.diagnostics_popout.borrow_mut() = Some( gtk::ApplicationWindow::builder() .application(&app) .title("Diagnostics") .build(), ); *view.log_popout.borrow_mut() = Some( gtk::ApplicationWindow::builder() .application(&app) .title("Log") .build(), ); shutdown_launcher_runtime( &child_proc, &tests, None, &view.widgets, &view.popouts, &view.diagnostics_popout, &view.log_popout, ); assert!(view.popouts.borrow().iter().all(|handle| handle.is_none())); assert!( view.widgets.display_panes[0] .preview_binding .borrow() .is_none() ); assert!( view.widgets.display_panes[1] .preview_binding .borrow() .is_none() ); assert!(view.diagnostics_popout.borrow().is_none()); assert!(view.log_popout.borrow().is_none()); }