//! Contract tests for the launcher layout proportions. //! //! Scope: statically guard the GTK layout constants and sizing glue used by //! the launcher shell. //! Targets: `client/src/launcher/ui_components.rs`. //! Why: the launcher is an operational control surface; accidental spacing //! regressions can hide diagnostics or make eye/device previews unusable. const UI_SRC: &str = include_str!("../../client/src/launcher/ui_components.rs"); fn const_i32(name: &str) -> i32 { let needle = format!("const {name}: i32 = "); let line = UI_SRC .lines() .find(|line| line.trim_start().starts_with(&needle)) .unwrap_or_else(|| panic!("missing {name} constant")); line.trim_start() .trim_start_matches(&needle) .trim_end_matches(';') .parse() .unwrap_or_else(|err| panic!("invalid {name} constant: {err}")) } fn source_index(needle: &str) -> usize { UI_SRC .find(needle) .unwrap_or_else(|| panic!("missing source marker: {needle}")) } #[test] fn launcher_default_size_stays_inside_1080p() { assert_eq!(const_i32("LAUNCHER_DEFAULT_WIDTH"), 1360); assert_eq!(const_i32("LAUNCHER_DEFAULT_HEIGHT"), 820); assert!(const_i32("LAUNCHER_DEFAULT_WIDTH") <= 1920); assert!(const_i32("LAUNCHER_DEFAULT_HEIGHT") <= 1080); } #[test] fn eye_panes_keep_the_locked_larger_preview_footprint() { assert_eq!(const_i32("EYE_PREVIEW_MIN_WIDTH"), 460); assert_eq!(const_i32("EYE_PREVIEW_MIN_HEIGHT"), 258); assert!( UI_SRC.contains("caption_label.set_halign(gtk::Align::End)") || UI_SRC.contains("capture_label.set_halign(gtk::Align::End)") ); assert!(UI_SRC.contains("capture_label.set_ellipsize(pango::EllipsizeMode::Start);")); } #[test] fn device_staging_and_testing_bottoms_stay_locked_together() { assert!(UI_SRC.contains("staging_row.set_homogeneous(true);")); assert!(UI_SRC.contains("staging_row.set_vexpand(false);")); assert!(UI_SRC.contains("devices_panel.set_valign(gtk::Align::Fill);")); assert!(UI_SRC.contains("preview_panel.set_valign(gtk::Align::Fill);")); assert!(UI_SRC.contains( "let device_body_height_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Vertical);" )); assert!(UI_SRC.contains("device_body_height_group.add_widget(&devices_body);")); assert!(UI_SRC.contains("device_body_height_group.add_widget(&testing_row);")); } #[test] fn device_testing_keeps_webcam_and_mic_playback_as_equal_bottom_columns() { assert_eq!(const_i32("CAMERA_PREVIEW_VIEWPORT_WIDTH"), 280); assert_eq!(const_i32("CAMERA_PREVIEW_VIEWPORT_HEIGHT"), 158); assert!(UI_SRC.contains("webcam_group.set_valign(gtk::Align::Fill);")); assert!(UI_SRC.contains("playback_group.set_valign(gtk::Align::Fill);")); assert!(UI_SRC.contains("preview_body.set_vexpand(false);")); assert!(UI_SRC.contains("playback_body.set_valign(gtk::Align::Fill);")); assert!(UI_SRC.contains("audio_check_meter.set_vexpand(true);")); assert!(UI_SRC.contains("playback_body.append(&audio_check_meter);")); assert!(UI_SRC.contains("playback_body.append(µphone_replay_button);")); } #[test] fn operations_column_fills_height_and_splits_extra_space_between_logs() { assert_eq!(const_i32("SIDE_LOG_HEIGHT"), 124); assert!(UI_SRC.contains("operations.set_vexpand(true);")); assert!(UI_SRC.contains("operations.set_valign(gtk::Align::Fill);")); assert_eq!( UI_SRC .matches(".min_content_height(SIDE_LOG_HEIGHT)") .count(), 2 ); assert_eq!( UI_SRC .matches(".max_content_height(SIDE_LOG_HEIGHT)") .count(), 2 ); } #[test] fn relay_controls_keep_connect_inline_with_server_entry() { assert!(UI_SRC.contains("build_panel(\"Relay Controls\")")); assert!(UI_SRC.contains("let relay_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);")); assert!(UI_SRC.contains("relay_row.append(&server_entry);")); assert!(UI_SRC.contains("let start_button = gtk::Button::with_label(\"Connect\");")); assert!(UI_SRC.contains("relay_row.append(&start_button);")); assert!( source_index("relay_row.append(&server_entry);") < source_index("relay_row.append(&start_button);") ); } #[test] fn remote_audio_gain_control_stays_in_the_operations_rail() { assert!(!UI_SRC.contains("Remote Audio")); assert!(UI_SRC.contains("let power_shell = gtk::Box::new(gtk::Orientation::Vertical, 6);")); assert!(UI_SRC.contains("let power_heading = gtk::Label::new(Some(\"Power\"));")); assert!(UI_SRC.contains("power_row.append(&power_heading);")); assert!(UI_SRC.contains("let audio_gain_scale =")); assert!(UI_SRC.contains("audio_gain_scale.set_draw_value(false);")); assert!(UI_SRC.contains("audio_gain_value.set_width_chars(5);")); assert!(UI_SRC.contains("audio_gain_row.set_size_request(220, -1);")); assert!(UI_SRC.contains("power_shell.append(&audio_gain_row);")); assert_eq!( UI_SRC .matches("connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));") .count(), 2, "the operations rail should not gain extra vertical sections that stretch the lower layout" ); assert!( source_index("let power_heading = gtk::Label::new(Some(\"Power\"));") < source_index("let audio_gain_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);") ); assert!( source_index("power_shell.append(&audio_gain_row);") < source_index("let routing_heading = gtk::Label::new(Some(\"Input\"));") ); }