use std::time::Duration;
use flow_wm::ipc::message::SocketMessage;
use super::common::unique_pipe_name;
use super::test_desktop::{
DaemonGuard, TestDesktop, TestWindow, query_layout_virtual, query_windows, send_ipc_retry,
start_test_daemon, unique_title,
};
fn hwnd_id(w: &TestWindow) -> i64 {
w.hwnd.0 as i64
}
fn focus_left(pipe: &str) {
let _ = send_ipc_retry(pipe, &SocketMessage::FocusLeft);
}
fn layout_window_ids(json: &serde_json::Value) -> Vec<i64> {
json["columns"]
.as_array()
.expect("columns array")
.iter()
.flat_map(|col| {
col["rows"]
.as_array()
.expect("rows array")
.iter()
.filter_map(|r| r["window_id"].as_i64())
})
.collect()
}
fn tiled_rect_x(json: &serde_json::Value, id: i64) -> Option<i64> {
json["windows"]
.as_array()?
.iter()
.find(|w| w["hwnd"].as_i64() == Some(id))?["tiled_rect"]["x"]
.as_i64()
}
fn settle() {
std::thread::sleep(Duration::from_millis(500));
}
#[test]
fn destroy_removes_column_and_compacts_layout() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
settle();
let t1 = unique_title("RM-A");
let t2 = unique_title("RM-B");
let t3 = unique_title("RM-C");
let w1 = TestWindow::create(&t1).expect("create w1");
let w2 = TestWindow::create(&t2).expect("create w2");
let w3 = TestWindow::create(&t3).expect("create w3");
std::thread::sleep(Duration::from_millis(1500));
let (w1_id, w2_id, w3_id) = (hwnd_id(&w1), hwnd_id(&w2), hwnd_id(&w3));
let vl_before = query_layout_virtual(&pipe).expect("query virtual before");
let col_count_before = vl_before["column_count"].as_u64();
assert!(
col_count_before == Some(3),
"expected 3 columns after creating 3 windows, got {col_count_before:?}\n\
layout: {vl_before:#?}"
);
settle();
let json_before = query_windows(&pipe).expect("query windows before");
let w3_x_before = tiled_rect_x(&json_before, w3_id)
.unwrap_or_else(|| panic!("w3 (hwnd={w3_id}) should have a tiled_rect: {json_before:#?}"));
drop(w2);
std::thread::sleep(Duration::from_millis(1500));
let vl_after = query_layout_virtual(&pipe).expect("query virtual after");
let col_count_after = vl_after["column_count"].as_u64();
assert_eq!(
col_count_after,
Some(2),
"expected 2 columns after destroying middle window, got {col_count_after:?}\n\
layout: {vl_after:#?}"
);
let ids = layout_window_ids(&vl_after);
assert!(
ids.contains(&w1_id),
"w1 (hwnd={w1_id}) should remain in layout: ids={ids:?}"
);
assert!(
ids.contains(&w3_id),
"w3 (hwnd={w3_id}) should remain in layout: ids={ids:?}"
);
assert!(
!ids.contains(&w2_id),
"w2 (hwnd={w2_id}) should be gone from layout: ids={ids:?}"
);
settle();
let json_after = query_windows(&pipe).expect("query windows after");
let w3_x_after = tiled_rect_x(&json_after, w3_id).unwrap_or_else(|| {
panic!("w3 should still have a tiled_rect after removal: {json_after:#?}")
});
assert!(
w3_x_after < w3_x_before,
"w3 should shift left after removing middle column \
(before x={w3_x_before}, after x={w3_x_after})"
);
println!(
"✓ destroy_removes_column_and_compacts_layout: \
columns {col_count_before:?}→{col_count_after:?}, \
w3 x {w3_x_before}→{w3_x_after}"
);
drop(w1);
drop(w3);
drop(td);
}
#[ignore = "isolated desktop blocks SetForegroundWindow; registry focus can't move via FocusLeft (environmental)"]
#[test]
fn destroy_focused_window_moves_focus_left() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
settle();
let t1 = unique_title("FOC-A");
let t2 = unique_title("FOC-B");
let t3 = unique_title("FOC-C");
let w1 = TestWindow::create(&t1).expect("create w1");
let w2 = TestWindow::create(&t2).expect("create w2");
let w3 = TestWindow::create(&t3).expect("create w3");
std::thread::sleep(Duration::from_millis(1500));
let (w1_id, w2_id) = (hwnd_id(&w1), hwnd_id(&w2));
focus_left(&pipe);
settle();
let json_before = query_windows(&pipe).expect("query before destroy");
let focused_before = json_before["focused"].as_i64();
assert_eq!(
focused_before,
Some(w2_id),
"focus should be on w2 (hwnd={w2_id}) after FocusLeft, \
got {focused_before:?}\njson: {json_before:#?}"
);
drop(w2);
std::thread::sleep(Duration::from_millis(1500));
let json_after = query_windows(&pipe).expect("query after destroy");
let focused_after = json_after["focused"].as_i64();
assert_eq!(
focused_after,
Some(w1_id),
"focus should move to w1 (hwnd={w1_id}) after destroying focused w2, \
got {focused_after:?}\njson: {json_after:#?}"
);
println!(
"✓ destroy_focused_window_moves_focus_left: \
focus {w2_id} → {w1_id}"
);
drop(w1);
drop(w3);
drop(td);
}
#[test]
fn minimize_removes_window_from_layout() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
settle();
let t1 = unique_title("MIN-A");
let t2 = unique_title("MIN-B");
let t3 = unique_title("MIN-C");
let w1 = TestWindow::create(&t1).expect("create w1");
let w2 = TestWindow::create(&t2).expect("create w2");
let w3 = TestWindow::create(&t3).expect("create w3");
std::thread::sleep(Duration::from_millis(1500));
let w2_id = hwnd_id(&w2);
let vl_before = query_layout_virtual(&pipe).expect("query virtual before");
assert_eq!(
vl_before["column_count"].as_u64(),
Some(3),
"3 columns expected"
);
w2.minimize();
std::thread::sleep(Duration::from_millis(1500));
let vl_after = query_layout_virtual(&pipe).expect("query virtual after");
let col_count_after = vl_after["column_count"].as_u64();
assert_eq!(
col_count_after,
Some(2),
"expected 2 columns after minimising w2, got {col_count_after:?}\n\
layout: {vl_after:#?}"
);
let ids = layout_window_ids(&vl_after);
assert!(
!ids.contains(&w2_id),
"w2 (hwnd={w2_id}) should be removed from layout after minimise: ids={ids:?}"
);
println!(
"✓ minimize_removes_window_from_layout: \
columns 3 → {col_count_after:?}"
);
drop(w1);
drop(w2);
drop(w3);
drop(td);
}
#[test]
fn destroy_only_window_clears_focus() {
let td = TestDesktop::create().expect("test desktop");
let pipe = unique_pipe_name();
let mut _child = start_test_daemon(&pipe, &td.name).expect("start daemon");
let _guard = DaemonGuard::new(&pipe);
settle();
let t1 = unique_title("SOLO");
let w1 = TestWindow::create(&t1).expect("create w1");
std::thread::sleep(Duration::from_millis(1500));
let w1_id = hwnd_id(&w1);
let json_before = query_windows(&pipe).expect("query before");
assert_eq!(
json_before["focused"].as_i64(),
Some(w1_id),
"focus should be on the only window"
);
drop(w1);
std::thread::sleep(Duration::from_millis(1500));
let json_after = query_windows(&pipe).expect("query after");
let focused_after = &json_after["focused"];
assert!(
focused_after.is_null(),
"focus should be null after destroying the only window, got {focused_after:?}"
);
println!("✓ destroy_only_window_clears_focus: focus → null");
drop(td);
}