//! Integration coverage for the client window layout contract. //! //! Scope: exercise `layout::apply` end-to-end against a fake `swaymsg` binary. //! Targets: `client/src/layout.rs`. //! Why: this keeps layout policy coverage centralized in `testing/tests` while //! avoiding source-LOC ratchet growth in the client crate. use lesavka_client::layout::{Layout, apply}; use serial_test::serial; use std::fs; use std::os::unix::fs::PermissionsExt; use temp_env::with_var; use tempfile::tempdir; fn install_sway_stub(dir: &std::path::Path) -> std::path::PathBuf { let sway_path = dir.join("swaymsg"); fs::write( &sway_path, r#"#!/usr/bin/env bash set -euo pipefail if [[ "${1:-}" == "-t" && "${2:-}" == "get_outputs" && "${3:-}" == "-r" ]]; then printf '%s\n' "${LESAVKA_TEST_SWAY_OUTPUTS:-[]}" exit 0 fi echo "$*" >> "${LESAVKA_TEST_SWAY_LOG}" "#, ) .expect("write swaymsg stub"); let mut perms = fs::metadata(&sway_path) .expect("read swaymsg stub metadata") .permissions(); perms.set_mode(0o755); fs::set_permissions(&sway_path, perms).expect("mark swaymsg stub executable"); sway_path } fn run_apply_with_outputs(layout: Layout, outputs_json: &str) -> Option { let dir = tempdir().expect("create temp dir"); install_sway_stub(dir.path()); let log_path = dir.path().join("sway.log"); let old_path = std::env::var("PATH").unwrap_or_default(); let new_path = format!("{}:{old_path}", dir.path().display()); let log_value = log_path.to_string_lossy().to_string(); with_var("PATH", Some(new_path.as_str()), || { with_var("LESAVKA_TEST_SWAY_LOG", Some(log_value.as_str()), || { with_var("LESAVKA_TEST_SWAY_OUTPUTS", Some(outputs_json), || { apply(layout); }); }); }); fs::read_to_string(log_path).ok() } #[test] #[serial] fn side_by_side_places_each_eye_on_half_of_focused_output() { let outputs = r#"[{"focused":true,"rect":{"x":10,"y":20,"width":1920,"height":1080}}]"#; let log = run_apply_with_outputs(Layout::SideBySide, outputs).expect("read command log"); assert!(log.contains(r#"[title="^Lesavka-eye-0$"] resize set 960 1080; move position 10 20"#)); assert!(log.contains(r#"[title="^Lesavka-eye-1$"] resize set 960 1080; move position 970 20"#)); } #[test] #[serial] fn full_layout_modes_hide_the_opposite_eye_offscreen() { let outputs = r#"[{"focused":true,"rect":{"x":100,"y":50,"width":640,"height":360}}]"#; let left = run_apply_with_outputs(Layout::FullLeft, outputs).expect("left layout log"); assert!(left.contains(r#"[title="^Lesavka-eye-0$"] resize set 640 360; move position 100 50"#)); assert!(left.contains(r#"[title="^Lesavka-eye-1$"] resize set 1 1; move position 1380 770"#)); let right = run_apply_with_outputs(Layout::FullRight, outputs).expect("right layout log"); assert!( right.contains(r#"[title="^Lesavka-eye-1$"] resize set 640 360; move position 100 50"#) ); assert!(right.contains(r#"[title="^Lesavka-eye-0$"] resize set 1 1; move position 1380 770"#)); } #[test] #[serial] fn apply_skips_window_commands_when_outputs_payload_is_invalid() { let log = run_apply_with_outputs(Layout::SideBySide, "{not-json").unwrap_or_default(); assert!( log.trim().is_empty(), "expected no placement calls, got: {log}" ); } #[test] #[serial] fn apply_skips_window_commands_when_no_output_is_focused() { let outputs = r#"[{"focused":false,"rect":{"x":0,"y":0,"width":1920,"height":1080}}]"#; let log = run_apply_with_outputs(Layout::SideBySide, outputs).unwrap_or_default(); assert!( log.trim().is_empty(), "expected no placement calls, got: {log}" ); }