2026-04-23 07:00:06 -03:00
|
|
|
use super::*;
|
|
|
|
|
use crate::launcher::{
|
2026-04-23 14:15:02 -03:00
|
|
|
devices::{CameraMode, DeviceCatalog},
|
|
|
|
|
preview::PreviewBinding,
|
2026-04-23 15:18:30 -03:00
|
|
|
state::{BreakoutSizePreset, LauncherState, PreviewSourceSize},
|
2026-04-23 07:00:06 -03:00
|
|
|
ui_components::build_launcher_view,
|
|
|
|
|
};
|
2026-04-30 15:04:00 -03:00
|
|
|
use crate::uplink_telemetry::UpstreamStreamTelemetry;
|
2026-04-23 11:49:19 -03:00
|
|
|
use gtk::prelude::*;
|
2026-04-23 07:00:06 -03:00
|
|
|
use serial_test::serial;
|
2026-04-23 15:18:30 -03:00
|
|
|
use std::{
|
|
|
|
|
cell::RefCell,
|
|
|
|
|
collections::BTreeMap,
|
|
|
|
|
rc::Rc,
|
|
|
|
|
time::{Duration, Instant},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fn present_and_settle(window: >k::ApplicationWindow) {
|
|
|
|
|
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));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-23 07:00:06 -03:00
|
|
|
|
|
|
|
|
#[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");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 11:49:19 -03:00
|
|
|
#[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,
|
|
|
|
|
);
|
2026-04-23 15:18:30 -03:00
|
|
|
present_and_settle(&view.window);
|
2026-04-23 11:49:19 -03:00
|
|
|
|
|
|
|
|
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!(
|
2026-04-29 20:52:55 -03:00
|
|
|
min_width <= 1600 && view.window.width() <= 1600,
|
2026-04-23 11:49:19 -03:00
|
|
|
"launcher width budget regressed: min={min_width}, natural={natural_width}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
2026-04-29 20:52:55 -03:00
|
|
|
min_height <= 980 && view.window.height() <= 980,
|
2026-04-23 11:49:19 -03:00
|
|
|
"launcher height budget regressed: min={min_height}, natural={natural_height}"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 14:15:02 -03:00
|
|
|
#[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);
|
2026-04-23 15:18:30 -03:00
|
|
|
present_and_settle(&view.window);
|
2026-04-23 14:15:02 -03:00
|
|
|
|
|
|
|
|
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!(
|
2026-04-29 20:52:55 -03:00
|
|
|
min_width <= 1600 && view.window.width() <= 1600,
|
2026-04-23 14:15:02 -03:00
|
|
|
"populated launcher width budget regressed: min={min_width}, natural={natural_width}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
2026-04-29 20:52:55 -03:00
|
|
|
min_height <= 980 && view.window.height() <= 980,
|
2026-04-23 14:15:02 -03:00
|
|
|
"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);
|
2026-04-23 15:18:30 -03:00
|
|
|
present_and_settle(&view.window);
|
2026-04-23 14:15:02 -03:00
|
|
|
|
|
|
|
|
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
|
2026-04-23 17:03:12 -03:00
|
|
|
.measure(gtk::Orientation::Vertical, 420);
|
2026-04-23 14:15:02 -03:00
|
|
|
let (testing_panel_min_h, testing_panel_nat_h, _, _) = view
|
|
|
|
|
.device_stage
|
2026-04-23 15:18:30 -03:00
|
|
|
.preview_panel
|
2026-04-23 17:03:12 -03:00
|
|
|
.measure(gtk::Orientation::Vertical, 520);
|
2026-04-23 14:15:02 -03:00
|
|
|
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
|
2026-04-23 17:03:12 -03:00
|
|
|
.measure(gtk::Orientation::Vertical, 720);
|
2026-04-23 14:15:02 -03:00
|
|
|
let (server_min_w, server_nat_w, _, _) =
|
|
|
|
|
view.server_entry.measure(gtk::Orientation::Horizontal, -1);
|
|
|
|
|
|
|
|
|
|
assert!(
|
2026-04-23 17:03:12 -03:00
|
|
|
camera_min_w >= 400 && camera_nat_w >= 400,
|
2026-04-23 14:15:02 -03:00
|
|
|
"camera preview width regressed: min={camera_min_w}, natural={camera_nat_w}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
2026-04-23 17:03:12 -03:00
|
|
|
camera_min_h >= 225 && camera_nat_h >= 225,
|
2026-04-23 14:15:02 -03:00
|
|
|
"camera preview height regressed: min={camera_min_h}, natural={camera_nat_h}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
2026-04-23 17:03:12 -03:00
|
|
|
testing_panel_min_h <= 420 && testing_panel_nat_h <= 420,
|
2026-04-23 14:15:02 -03:00
|
|
|
"device testing panel height regressed: min={testing_panel_min_h}, natural={testing_panel_nat_h}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
2026-04-23 17:03:12 -03:00
|
|
|
left_min_w <= 700 && left_nat_w <= 720,
|
2026-04-23 14:15:02 -03:00
|
|
|
"eye pane width regressed: min={left_min_w}, natural={left_nat_w}"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
2026-04-23 17:03:12 -03:00
|
|
|
left_min_h <= 700 && left_nat_h <= 700,
|
2026-04-23 14:15:02 -03:00
|
|
|
"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}"
|
|
|
|
|
);
|
2026-04-23 15:18:30 -03:00
|
|
|
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()
|
|
|
|
|
);
|
2026-04-23 19:27:04 -03:00
|
|
|
assert_eq!(
|
|
|
|
|
view.device_stage
|
|
|
|
|
.camera_preview_stack
|
|
|
|
|
.visible_child_name()
|
|
|
|
|
.as_deref(),
|
|
|
|
|
Some("idle"),
|
|
|
|
|
"idle launcher should show the placeholder webcam surface until preview wakes up"
|
|
|
|
|
);
|
|
|
|
|
view.device_stage
|
|
|
|
|
.camera_preview_stack
|
|
|
|
|
.set_visible_child_name("live");
|
|
|
|
|
present_and_settle(&view.window);
|
2026-04-23 15:18:30 -03:00
|
|
|
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()
|
|
|
|
|
);
|
2026-04-23 17:03:12 -03:00
|
|
|
assert!(
|
|
|
|
|
view.widgets.display_panes[0].preview_frame.width()
|
|
|
|
|
>= view.device_stage.camera_preview_frame.width(),
|
|
|
|
|
"eye preview should stay at least as wide as the webcam preview: eye={}, webcam={}",
|
|
|
|
|
view.widgets.display_panes[0].preview_frame.width(),
|
|
|
|
|
view.device_stage.camera_preview_frame.width()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
view.widgets.display_panes[0].root.width()
|
|
|
|
|
- view.widgets.display_panes[0].preview_frame.width()
|
|
|
|
|
<= 60,
|
|
|
|
|
"eye preview stopped filling its card width: card={}, preview={}",
|
|
|
|
|
view.widgets.display_panes[0].root.width(),
|
|
|
|
|
view.widgets.display_panes[0].preview_frame.width()
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
view.device_stage.camera_preview_frame.height() >= 225,
|
|
|
|
|
"webcam preview should use the taller upstream media space: {}",
|
|
|
|
|
view.device_stage.camera_preview_frame.height()
|
|
|
|
|
);
|
2026-04-23 15:18:30 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-25 06:56:17 -03:00
|
|
|
#[gtk::test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn launcher_shell_installs_native_window_chrome() {
|
|
|
|
|
if gtk::gdk::Display::default().is_none() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let app = gtk::Application::builder()
|
|
|
|
|
.application_id("dev.lesavka.test-window-chrome")
|
|
|
|
|
.build();
|
|
|
|
|
let _ = app.register(None::<>k::gio::Cancellable>);
|
|
|
|
|
|
|
|
|
|
let view = build_launcher_view(
|
|
|
|
|
&app,
|
|
|
|
|
"http://127.0.0.1:50051",
|
|
|
|
|
&DeviceCatalog::default(),
|
|
|
|
|
&LauncherState::new(),
|
|
|
|
|
);
|
|
|
|
|
present_and_settle(&view.window);
|
|
|
|
|
|
|
|
|
|
let titlebar = view
|
|
|
|
|
.window
|
|
|
|
|
.titlebar()
|
|
|
|
|
.expect("launcher titlebar")
|
|
|
|
|
.downcast::<gtk::HeaderBar>()
|
|
|
|
|
.expect("launcher header bar");
|
|
|
|
|
assert!(view.window.is_decorated());
|
|
|
|
|
assert!(titlebar.shows_title_buttons());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[gtk::test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn diagnostics_and_log_popouts_install_native_window_chrome() {
|
|
|
|
|
if gtk::gdk::Display::default().is_none() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let app = gtk::Application::builder()
|
|
|
|
|
.application_id("dev.lesavka.test-popout-chrome")
|
|
|
|
|
.build();
|
|
|
|
|
let _ = app.register(None::<>k::gio::Cancellable>);
|
|
|
|
|
|
|
|
|
|
let diagnostics_handle = Rc::new(RefCell::new(None::<gtk::ApplicationWindow>));
|
|
|
|
|
let diagnostics_label = Rc::new(RefCell::new(None::<gtk::Label>));
|
|
|
|
|
let diagnostics_scroll = Rc::new(RefCell::new(None::<gtk::ScrolledWindow>));
|
|
|
|
|
let rendered_text = Rc::new(RefCell::new("diagnostics".to_string()));
|
|
|
|
|
|
|
|
|
|
open_diagnostics_popout(
|
|
|
|
|
&app,
|
|
|
|
|
&diagnostics_handle,
|
|
|
|
|
&diagnostics_label,
|
|
|
|
|
&diagnostics_scroll,
|
|
|
|
|
&rendered_text,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let diagnostics_window = diagnostics_handle
|
|
|
|
|
.borrow()
|
|
|
|
|
.as_ref()
|
|
|
|
|
.expect("diagnostics window")
|
|
|
|
|
.clone();
|
|
|
|
|
let diagnostics_titlebar = diagnostics_window
|
|
|
|
|
.titlebar()
|
|
|
|
|
.expect("diagnostics titlebar")
|
|
|
|
|
.downcast::<gtk::HeaderBar>()
|
|
|
|
|
.expect("diagnostics header bar");
|
|
|
|
|
assert!(diagnostics_window.is_decorated());
|
|
|
|
|
assert!(diagnostics_titlebar.shows_title_buttons());
|
|
|
|
|
|
|
|
|
|
let log_handle = Rc::new(RefCell::new(None::<gtk::ApplicationWindow>));
|
|
|
|
|
let log_scroll = Rc::new(RefCell::new(None::<gtk::ScrolledWindow>));
|
|
|
|
|
let log_buffer = gtk::TextBuffer::new(None);
|
|
|
|
|
log_buffer.set_text("session log");
|
|
|
|
|
|
|
|
|
|
open_session_log_popout(&app, &log_handle, &log_buffer);
|
|
|
|
|
|
|
|
|
|
let log_window = log_handle.borrow().as_ref().expect("log window").clone();
|
|
|
|
|
let log_titlebar = log_window
|
|
|
|
|
.titlebar()
|
|
|
|
|
.expect("log titlebar")
|
|
|
|
|
.downcast::<gtk::HeaderBar>()
|
|
|
|
|
.expect("log header bar");
|
|
|
|
|
assert!(log_window.is_decorated());
|
|
|
|
|
assert!(log_titlebar.shows_title_buttons());
|
|
|
|
|
|
|
|
|
|
diagnostics_window.close();
|
|
|
|
|
log_window.close();
|
|
|
|
|
diagnostics_label.borrow_mut().take();
|
|
|
|
|
diagnostics_scroll.borrow_mut().take();
|
|
|
|
|
log_scroll.borrow_mut().take();
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 15:18:30 -03:00
|
|
|
#[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);
|
2026-04-23 14:15:02 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
#[test]
|
|
|
|
|
fn server_chip_state_tracks_connection_not_just_reachability() {
|
|
|
|
|
let mut state = LauncherState::new();
|
|
|
|
|
assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
|
2026-04-29 23:19:57 -03:00
|
|
|
assert_eq!(server_version_label(&state), "???");
|
2026-04-23 07:00:06 -03:00
|
|
|
|
|
|
|
|
state.set_server_available(true);
|
2026-04-29 20:52:55 -03:00
|
|
|
state.set_server_version(Some(crate::VERSION.to_string()));
|
2026-04-23 11:14:58 -03:00
|
|
|
assert_eq!(server_light_state(&state, false), StatusLightState::Live);
|
2026-04-29 20:52:55 -03:00
|
|
|
assert_eq!(server_version_label(&state), format!("v{}", crate::VERSION));
|
2026-04-23 07:00:06 -03:00
|
|
|
|
2026-04-23 11:14:58 -03:00
|
|
|
assert_eq!(
|
|
|
|
|
server_light_state(&state, true),
|
|
|
|
|
StatusLightState::Connected
|
|
|
|
|
);
|
2026-04-23 07:00:06 -03:00
|
|
|
|
2026-04-24 00:30:07 -03:00
|
|
|
state.set_server_version(Some("v0.12.5".to_string()));
|
2026-04-23 11:14:58 -03:00
|
|
|
assert_eq!(server_light_state(&state, false), StatusLightState::Warning);
|
|
|
|
|
assert_eq!(server_light_state(&state, true), StatusLightState::Caution);
|
2026-04-24 00:30:07 -03:00
|
|
|
assert_eq!(server_version_label(&state), "v0.12.5");
|
2026-04-23 07:00:06 -03:00
|
|
|
|
|
|
|
|
state.set_server_version(Some(" ".to_string()));
|
2026-04-23 11:14:58 -03:00
|
|
|
assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
|
2026-04-29 23:19:57 -03:00
|
|
|
assert_eq!(server_version_label(&state), "???");
|
2026-04-23 07:00:06 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 15:04:00 -03:00
|
|
|
#[test]
|
|
|
|
|
fn uac_chip_uses_live_microphone_flow_not_only_server_caps() {
|
|
|
|
|
let mut state = LauncherState::new();
|
|
|
|
|
state.set_server_available(true);
|
|
|
|
|
state.set_server_media_caps(Some(true), Some(true), Some("uvc".to_string()), None);
|
|
|
|
|
state.set_microphone_channel_enabled(true);
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uac_health(&state, false, None),
|
|
|
|
|
(StatusLightState::Live, "Ready".to_string())
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uac_health(&state, true, None),
|
|
|
|
|
(StatusLightState::Caution, "No Flow".to_string())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let healthy = UpstreamStreamTelemetry {
|
|
|
|
|
enabled: true,
|
|
|
|
|
connected: true,
|
|
|
|
|
packets_streamed: 24,
|
|
|
|
|
latest_delivery_age_ms: 42.0,
|
|
|
|
|
latest_enqueue_age_ms: 12.0,
|
|
|
|
|
queue_depth: 1,
|
|
|
|
|
..UpstreamStreamTelemetry::default()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uac_health(&state, true, Some(&healthy)),
|
|
|
|
|
(StatusLightState::Live, "Flowing".to_string())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
state.set_microphone_channel_enabled(false);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uac_health(&state, true, Some(&healthy)),
|
|
|
|
|
(StatusLightState::Idle, "Paused".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn uvc_chip_degrades_when_live_camera_frames_are_not_flowing() {
|
|
|
|
|
let mut state = LauncherState::new();
|
|
|
|
|
state.set_server_available(true);
|
|
|
|
|
state.set_server_media_caps(
|
|
|
|
|
Some(true),
|
|
|
|
|
Some(true),
|
|
|
|
|
Some("uvc".to_string()),
|
|
|
|
|
Some("mjpeg".to_string()),
|
|
|
|
|
);
|
|
|
|
|
state.set_camera_channel_enabled(true);
|
|
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uvc_health(&state, false, None),
|
|
|
|
|
(StatusLightState::Live, "MJPEG".to_string())
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uvc_health(&state, true, None),
|
|
|
|
|
(StatusLightState::Caution, "No Frames".to_string())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let healthy = UpstreamStreamTelemetry {
|
|
|
|
|
enabled: true,
|
|
|
|
|
connected: true,
|
|
|
|
|
packets_streamed: 12,
|
|
|
|
|
latest_delivery_age_ms: 48.0,
|
|
|
|
|
latest_enqueue_age_ms: 20.0,
|
|
|
|
|
queue_depth: 3,
|
|
|
|
|
..UpstreamStreamTelemetry::default()
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uvc_health(&state, true, Some(&healthy)),
|
|
|
|
|
(StatusLightState::Live, "MJPEG".to_string())
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let lagging = UpstreamStreamTelemetry {
|
|
|
|
|
latest_delivery_age_ms: 321.0,
|
|
|
|
|
..healthy
|
|
|
|
|
};
|
|
|
|
|
assert_eq!(
|
|
|
|
|
recovery_uvc_health(&state, true, Some(&lagging)),
|
|
|
|
|
(StatusLightState::Caution, "Lagging".to_string())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
#[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}");
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 15:04:00 -03:00
|
|
|
#[test]
|
|
|
|
|
fn write_media_control_request_formats_soft_pause_state() {
|
|
|
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
|
|
|
let path = dir.path().join("media.control");
|
|
|
|
|
let mut state = LauncherState::new();
|
|
|
|
|
state.set_camera_channel_enabled(true);
|
|
|
|
|
state.set_microphone_channel_enabled(false);
|
|
|
|
|
state.set_audio_channel_enabled(true);
|
2026-05-02 10:31:22 -03:00
|
|
|
state.select_camera(Some("Logitech BRIO".to_string()));
|
|
|
|
|
state.select_camera_quality(Some(CameraMode::new(1280, 720, 30)));
|
|
|
|
|
state.select_microphone(Some(
|
|
|
|
|
"alsa_input.usb-Neat Microphones Bumblebee".to_string(),
|
|
|
|
|
));
|
|
|
|
|
state.select_speaker(Some("bluez_output.80_C3_BA_76_26_AB.1".to_string()));
|
2026-04-30 15:04:00 -03:00
|
|
|
|
|
|
|
|
write_media_control_request(&path, &state).expect("write media control");
|
|
|
|
|
let raw = std::fs::read_to_string(path).expect("read media control");
|
|
|
|
|
assert!(raw.contains("camera=1"), "{raw}");
|
|
|
|
|
assert!(raw.contains("microphone=0"), "{raw}");
|
|
|
|
|
assert!(raw.contains("audio=1"), "{raw}");
|
2026-05-02 10:31:22 -03:00
|
|
|
assert!(raw.contains("camera_source=b64:"), "{raw}");
|
|
|
|
|
assert!(raw.contains("camera_profile=b64:"), "{raw}");
|
|
|
|
|
assert!(raw.contains("microphone_source=b64:"), "{raw}");
|
|
|
|
|
assert!(raw.contains("audio_sink=b64:"), "{raw}");
|
2026-04-30 15:04:00 -03:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
#[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,
|
|
|
|
|
);
|
2026-04-24 00:30:07 -03:00
|
|
|
present_and_settle(&view.window);
|
2026-04-23 07:00:06 -03:00
|
|
|
let child_proc = Rc::new(RefCell::new(None::<RelayChild>));
|
|
|
|
|
|
|
|
|
|
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(),
|
2026-04-23 15:18:30 -03:00
|
|
|
root: gtk::Box::new(gtk::Orientation::Vertical, 0),
|
2026-04-23 07:00:06 -03:00
|
|
|
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(),
|
2026-04-23 15:18:30 -03:00
|
|
|
root: gtk::Box::new(gtk::Orientation::Vertical, 0),
|
2026-04-23 07:00:06 -03:00
|
|
|
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::<RelayChild>));
|
|
|
|
|
|
|
|
|
|
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,
|
2026-04-23 15:18:30 -03:00
|
|
|
root: gtk::Box::new(gtk::Orientation::Vertical, 0),
|
2026-04-23 07:00:06 -03:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-24 00:30:07 -03:00
|
|
|
#[gtk::test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn dock_display_to_preview_restores_closed_eye_placeholder_when_relay_is_idle() {
|
|
|
|
|
if gtk::gdk::Display::default().is_none() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let app = gtk::Application::builder()
|
|
|
|
|
.application_id("dev.lesavka.test-dock-placeholder")
|
|
|
|
|
.build();
|
|
|
|
|
let _ = app.register(None::<>k::gio::Cancellable>);
|
|
|
|
|
|
|
|
|
|
let state = Rc::new(RefCell::new(LauncherState::new()));
|
|
|
|
|
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::<RelayChild>));
|
|
|
|
|
|
|
|
|
|
*view.widgets.display_panes[1].preview_binding.borrow_mut() = Some(PreviewBinding::test_stub());
|
|
|
|
|
view.widgets.display_panes[1]
|
|
|
|
|
.preview_placeholder
|
|
|
|
|
.set_visible(false);
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let mut popouts = view.popouts.borrow_mut();
|
|
|
|
|
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: PreviewBinding::test_stub(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dock_display_to_preview(&state, &child_proc, &view.popouts, &view.widgets, 1);
|
|
|
|
|
|
|
|
|
|
present_and_settle(&view.window);
|
|
|
|
|
|
|
|
|
|
assert_eq!(state.borrow().display_surface(1), DisplaySurface::Preview);
|
|
|
|
|
assert!(view.popouts.borrow()[1].is_none());
|
|
|
|
|
assert!(
|
|
|
|
|
view.widgets.display_panes[1].picture.paintable().is_none(),
|
|
|
|
|
"idle docked pane should not retain a stale preview texture"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
view.widgets.display_panes[1]
|
|
|
|
|
.preview_placeholder
|
|
|
|
|
.is_visible(),
|
|
|
|
|
"closed-eye placeholder should return when docking back after disconnect"
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
view.widgets.display_panes[1].stream_status.text(),
|
|
|
|
|
"Connect relay to preview."
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 14:15:02 -03:00
|
|
|
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::<BTreeMap<_, _>>(),
|
|
|
|
|
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()],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
#[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::<RelayChild>));
|
|
|
|
|
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(),
|
2026-04-23 15:18:30 -03:00
|
|
|
root: gtk::Box::new(gtk::Orientation::Vertical, 0),
|
2026-04-23 07:00:06 -03:00
|
|
|
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());
|
|
|
|
|
}
|