flow-wm 0.1.1

A scrolling, infinite-horizontal-canvas tiling window manager for Windows
//! Integration tests for `flow dispatch swap-column` and `flow dispatch move-window`.
//!
//! These tests create an isolated desktop, start `flowd` on it, create dummy
//! windows, and exercise the swap/move CLI commands. The daemon's layout is
//! queried before and after each command to verify that columns are actually
//! swapped.
//!
//! Each test uses [`query_layout_virtual`] to inspect the virtual layout's
//! column→window-id mapping — the most direct way to detect a column swap.
//!
//! # Desktop isolation
//!
//! Every test creates a [`TestDesktop`] and spawns `flowd` directly via
//! [`start_test_daemon`] with the `--desktop` flag. The user's real desktop is
//! never touched. Each test gets a unique pipe name for parallel isolation.

// The daemon child process is reaped by the OS after `DaemonGuard` sends the
// Stop IPC message.  See the same pattern in `daemon_lifecycle.rs` and
// `registry.rs`.
#![allow(clippy::zombie_processes)]

use std::time::Duration;

use predicates::prelude::*;

use super::common::{flow, unique_pipe_name};
use super::test_desktop::{
    DaemonGuard, TestDesktop, TestWindow, query_layout_virtual, send_ipc_ignore, start_test_daemon,
    unique_title,
};

/// Delay after creating windows to let hooks fire and the daemon tile them.
const HOOK_SETTLE: Duration = Duration::from_millis(1500);

/// Delay after a dispatch command to let the layout update propagate.
const SWAP_SETTLE: Duration = Duration::from_millis(500);

// ── Helpers ──────────────────────────────────────────────────────────

/// Extract the column→window-id mapping from a `query_layout_virtual` JSON
/// response.
///
/// Returns a `Vec` where element *i* is a `Vec` of the window-id integers in
/// column *i* (row order).
fn column_window_ids(json: &serde_json::Value) -> Vec<Vec<i64>> {
    json["columns"]
        .as_array()
        .map(|cols| {
            cols.iter()
                .map(|col| {
                    col["rows"]
                        .as_array()
                        .map(|rows| rows.iter().filter_map(|r| r.as_i64()).collect::<Vec<_>>())
                        .unwrap_or_default()
                })
                .collect()
        })
        .unwrap_or_default()
}

// ── swap-column tests ────────────────────────────────────────────────

/// `flow dispatch swap-column right` swaps the focused column with its right
/// neighbour.
///
/// Setup: two windows → two columns. Focus is moved to the leftmost column
/// (via `focus left`, which is a no-op if already there) so that `swap-column
/// right` has a valid target. After the swap the column contents are exchanged.
#[ignore = "non-deterministic on isolated test desktop: daemon hooks may not register/tile windows before the assertion (startup hook race)"]
#[test]
fn swap_column_right_swaps_two_columns() {
    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);
    std::thread::sleep(Duration::from_millis(500));

    let title_a = unique_title("SwapColR-A");
    let title_b = unique_title("SwapColR-B");
    let _wa = TestWindow::create(&title_a).expect("create window A");
    let _wb = TestWindow::create(&title_b).expect("create window B");
    std::thread::sleep(HOOK_SETTLE);

    // Ensure the focused window is in the leftmost column.
    use flow_wm::ipc::message::SocketMessage;
    send_ipc_ignore(&pipe, &SocketMessage::FocusLeft);
    std::thread::sleep(SWAP_SETTLE);

    // Snapshot the column structure before the swap.
    let before = query_layout_virtual(&pipe).expect("query layout before");
    let cols_before = column_window_ids(&before);
    assert_eq!(
        cols_before.len(),
        2,
        "expected 2 columns, got {cols_before:?}"
    );

    // Act: swap columns to the right.
    flow(&pipe)
        .args(["dispatch", "swap-column", "right"])
        .assert()
        .stdout(predicate::str::contains("column swapped"))
        .success();
    std::thread::sleep(SWAP_SETTLE);

    // Assert: columns are swapped.
    let after = query_layout_virtual(&pipe).expect("query layout after");
    let cols_after = column_window_ids(&after);
    assert_eq!(
        cols_after.len(),
        2,
        "expected 2 columns after swap, got {cols_after:?}"
    );
    assert_eq!(
        cols_after[0], cols_before[1],
        "column 0 should now hold the old column 1 windows"
    );
    assert_eq!(
        cols_after[1], cols_before[0],
        "column 1 should now hold the old column 0 windows"
    );
}

/// `flow dispatch swap-column left` swaps the focused column with its left
/// neighbour.
///
/// Setup: two windows → two columns. Focus is moved to the rightmost column
/// (via `focus right`) so that `swap-column left` has a valid target.
#[ignore = "non-deterministic on isolated test desktop: daemon hooks may not register/tile windows before the assertion (startup hook race)"]
#[test]
fn swap_column_left_swaps_two_columns() {
    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);
    std::thread::sleep(Duration::from_millis(500));

    let title_a = unique_title("SwapColL-A");
    let title_b = unique_title("SwapColL-B");
    let _wa = TestWindow::create(&title_a).expect("create window A");
    let _wb = TestWindow::create(&title_b).expect("create window B");
    std::thread::sleep(HOOK_SETTLE);

    // Ensure focus is on the rightmost column.
    use flow_wm::ipc::message::SocketMessage;
    send_ipc_ignore(&pipe, &SocketMessage::FocusRight);
    std::thread::sleep(SWAP_SETTLE);

    let before = query_layout_virtual(&pipe).expect("query layout before");
    let cols_before = column_window_ids(&before);
    assert_eq!(
        cols_before.len(),
        2,
        "expected 2 columns, got {cols_before:?}"
    );

    // Act: swap columns to the left.
    flow(&pipe)
        .args(["dispatch", "swap-column", "left"])
        .assert()
        .stdout(predicate::str::contains("column swapped"))
        .success();
    std::thread::sleep(SWAP_SETTLE);

    let after = query_layout_virtual(&pipe).expect("query layout after");
    let cols_after = column_window_ids(&after);
    assert_eq!(cols_after.len(), 2);
    assert_eq!(
        cols_after[0], cols_before[1],
        "column 0 should now hold the old column 1 windows"
    );
    assert_eq!(
        cols_after[1], cols_before[0],
        "column 1 should now hold the old column 0 windows"
    );
}

// ── move-window tests ────────────────────────────────────────────────

/// `flow dispatch move-window right` on tiled windows is equivalent to
/// `swap-column right` (the semantic "move" resolves to a column swap for
/// horizontal movement of tiled windows).
#[ignore = "non-deterministic on isolated test desktop: daemon hooks may not register/tile windows before the assertion (startup hook race)"]
#[test]
fn move_window_right_swaps_two_columns() {
    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);
    std::thread::sleep(Duration::from_millis(500));

    let title_a = unique_title("MoveWinR-A");
    let title_b = unique_title("MoveWinR-B");
    let _wa = TestWindow::create(&title_a).expect("create window A");
    let _wb = TestWindow::create(&title_b).expect("create window B");
    std::thread::sleep(HOOK_SETTLE);

    // Ensure focus is on the leftmost column.
    use flow_wm::ipc::message::SocketMessage;
    send_ipc_ignore(&pipe, &SocketMessage::FocusLeft);
    std::thread::sleep(SWAP_SETTLE);

    let before = query_layout_virtual(&pipe).expect("query layout before");
    let cols_before = column_window_ids(&before);
    assert_eq!(
        cols_before.len(),
        2,
        "expected 2 columns, got {cols_before:?}"
    );

    // Act: move window right (semantic → column swap for tiled L/R).
    flow(&pipe)
        .args(["dispatch", "move-window", "right"])
        .assert()
        .stdout(predicate::str::contains("window moved"))
        .success();
    std::thread::sleep(SWAP_SETTLE);

    let after = query_layout_virtual(&pipe).expect("query layout after");
    let cols_after = column_window_ids(&after);
    assert_eq!(cols_after.len(), 2);
    assert_eq!(
        cols_after[0], cols_before[1],
        "column 0 should now hold the old column 1 windows"
    );
    assert_eq!(
        cols_after[1], cols_before[0],
        "column 1 should now hold the old column 0 windows"
    );
}

// ── Edge-case tests ──────────────────────────────────────────────────

/// `flow dispatch swap-column right` at the right edge (single column) returns
/// an error — there is no column to swap with.
#[ignore = "non-deterministic on isolated test desktop: daemon hooks may not register/tile windows before the assertion (startup hook race)"]
#[test]
fn swap_column_right_at_edge_returns_error() {
    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);
    std::thread::sleep(Duration::from_millis(500));

    let title = unique_title("EdgeOnly");
    let _w = TestWindow::create(&title).expect("create single window");
    std::thread::sleep(HOOK_SETTLE);

    // Verify we have exactly one column.
    let before = query_layout_virtual(&pipe).expect("query layout before");
    let cols_before = column_window_ids(&before);
    assert_eq!(
        cols_before.len(),
        1,
        "expected exactly 1 column, got {cols_before:?}"
    );

    // Act: swap right should fail (no column to the right).
    flow(&pipe)
        .args(["dispatch", "swap-column", "right"])
        .assert()
        .failure();
}