srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/core/src/manager/tests.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/src/manager/tests.rs')
-rw-r--r--crates/core/src/manager/tests.rs395
1 files changed, 391 insertions, 4 deletions
diff --git a/crates/core/src/manager/tests.rs b/crates/core/src/manager/tests.rs
index aa2fb1a..9af0181 100644
--- a/crates/core/src/manager/tests.rs
+++ b/crates/core/src/manager/tests.rs
@@ -154,6 +154,114 @@
}
#[test]
+ fn ending_a_resize_remembers_the_new_size_for_the_apps_next_window() {
+ let mut wm = wm_with_monitor();
+ // Tiling layout, so `add_window` skips `SmartPlacement`'s grid/
+ // cascade sizing entirely and the asserted geometry below reflects
+ // only the remembered-size lookup itself, not incidental grid math.
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.app_id = "alacritty".into();
+ w.geometry = Rect::new(100, 100, 300, 200);
+ wm.add_window(w);
+ wm.start_resize(a, ResizeEdge::BottomRight, 400, 300);
+ wm.update_resize(500, 400);
+ wm.end_resize();
+
+ let b = wm.alloc_window_id();
+ let mut w2 = Window::new(b, "b");
+ w2.app_id = "alacritty".into();
+ // Whatever a backend would have hardcoded before calling add_window --
+ // the remembered size must win over this, not just supplement it.
+ w2.geometry = Rect::new(0, 0, 800, 600);
+ wm.add_window(w2);
+ let placed = wm.window(b).unwrap().geometry;
+ assert_eq!((placed.width, placed.height), (400, 300), "the second alacritty window must open at the size the first was resized to");
+ }
+
+ #[test]
+ fn remembered_size_is_keyed_by_app_id_not_shared_across_different_apps() {
+ let mut wm = wm_with_monitor();
+ // Tiling layout, so `add_window` skips `SmartPlacement`'s grid/
+ // cascade sizing entirely and the asserted geometry below reflects
+ // only the remembered-size lookup itself, not incidental grid math.
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.app_id = "alacritty".into();
+ w.geometry = Rect::new(100, 100, 300, 200);
+ wm.add_window(w);
+ wm.start_resize(a, ResizeEdge::BottomRight, 400, 300);
+ wm.update_resize(500, 400);
+ wm.end_resize();
+
+ let b = wm.alloc_window_id();
+ let mut w2 = Window::new(b, "b");
+ w2.app_id = "firefox".into();
+ w2.geometry = Rect::new(0, 0, 800, 600);
+ wm.add_window(w2);
+ let placed = wm.window(b).unwrap().geometry;
+ assert_eq!((placed.width, placed.height), (800, 600), "a different app's default size must be untouched by alacritty's remembered size");
+ }
+
+ #[test]
+ fn maximizing_then_unmaximizing_does_not_change_the_remembered_size() {
+ // Only an interactive drag-resize should update `remembered_sizes` --
+ // maximize/fullscreen have their own separate `restore_geometry` and
+ // are not "a size the user wants their next window to open at".
+ let mut wm = wm_with_monitor();
+ // Tiling layout, so `add_window` skips `SmartPlacement`'s grid/
+ // cascade sizing entirely and the asserted geometry below reflects
+ // only the remembered-size lookup itself, not incidental grid math.
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.app_id = "alacritty".into();
+ w.geometry = Rect::new(100, 100, 300, 200);
+ wm.add_window(w);
+ wm.toggle_maximize(a);
+ wm.toggle_maximize(a);
+
+ let b = wm.alloc_window_id();
+ let mut w2 = Window::new(b, "b");
+ w2.app_id = "alacritty".into();
+ w2.geometry = Rect::new(0, 0, 800, 600);
+ wm.add_window(w2);
+ let placed = wm.window(b).unwrap().geometry;
+ assert_eq!((placed.width, placed.height), (800, 600), "maximize/unmaximize alone must not have remembered anything");
+ }
+
+ #[test]
+ fn a_rules_explicit_geometry_still_wins_over_a_remembered_size() {
+ let mut wm = wm_with_monitor();
+ // Tiling layout, so `add_window` skips `SmartPlacement`'s grid/
+ // cascade sizing entirely and the asserted geometry below reflects
+ // only the remembered-size lookup itself, not incidental grid math.
+ wm.set_layout(wm.current_workspace(), "tiling");
+ let a = wm.alloc_window_id();
+ let mut w = Window::new(a, "a");
+ w.app_id = "alacritty".into();
+ w.geometry = Rect::new(100, 100, 300, 200);
+ wm.add_window(w);
+ wm.start_resize(a, ResizeEdge::BottomRight, 400, 300);
+ wm.update_resize(500, 400);
+ wm.end_resize();
+
+ wm.add_rule(WindowRule {
+ matcher: crate::rules::WindowMatch { class: Some("alacritty".into()), ..Default::default() },
+ actions: crate::rules::WindowRuleActions { geometry: Some(Rect::new(0, 0, 640, 480)), ..Default::default() },
+ });
+ let b = wm.alloc_window_id();
+ let mut w2 = Window::new(b, "b");
+ w2.app_id = "alacritty".into();
+ w2.geometry = Rect::new(0, 0, 800, 600);
+ wm.add_window(w2);
+ let placed = wm.window(b).unwrap().geometry;
+ assert_eq!((placed.width, placed.height), (640, 480), "a rule's explicit geometry is more specific and must win");
+ }
+
+ #[test]
fn toggle_maximize_restores_original_geometry() {
let mut wm = wm_with_monitor();
let a = wm.alloc_window_id();
@@ -294,6 +402,30 @@
}
#[test]
+ fn hit_test_does_not_see_through_a_covering_windows_content_to_a_lower_windows_edge() {
+ // Reported live: a resize edge (or other titlebar/border zone)
+ // could still be grabbed on a window that was fully covered by
+ // another window on top of it, as long as the covering window's
+ // own edges didn't happen to land on that exact point. `a`'s left
+ // resize edge sits at x=0; `b` is stacked on top and covers that
+ // point with its own real content, but `b`'s own edges are far
+ // away (left at x=-100, nowhere near x=0), so `b` itself doesn't
+ // register a hit there - the bug was falling through to `a`'s
+ // edge underneath instead of stopping at `b`'s opaque content.
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ let mut wa = Window::new(a, "a");
+ wa.geometry = Rect::new(0, 0, 400, 300);
+ wm.add_window(wa);
+ let b = wm.alloc_window_id();
+ let mut wb = Window::new(b, "b");
+ wb.geometry = Rect::new(-100, 0, 600, 300); // added later -> on top, fully covers a
+ wm.add_window(wb);
+
+ assert_eq!(wm.hit_test(0, 150), None, "a's edge must not be reachable through b's opaque content");
+ }
+
+ #[test]
fn per_window_resize_margin_overrides_the_wm_wide_default() {
// Hyprland's per-window `extend_border_grab_area` equivalent.
let mut wm = wm_with_monitor();
@@ -389,10 +521,10 @@
let ws2 = wm.add_workspace("2", "dynamic");
wm.switch_workspace(ws2);
assert_eq!(wm.current_workspace(), ws2);
- // Re-selecting the already-active workspace jumps back to 0, the
+ // Re-selecting the already-active workspace jumps back to 1, the
// one that was active right before.
wm.switch_workspace(ws2);
- assert_eq!(wm.current_workspace(), 0);
+ assert_eq!(wm.current_workspace(), 1);
}
#[test]
@@ -405,6 +537,129 @@
}
#[test]
+ fn switching_to_a_workspace_with_a_window_focuses_it() {
+ // Regression test: `switch_workspace` used to only ever touch
+ // `current_workspace`, never `self.focused` - reported live as
+ // switching to a workspace with an open window leaving that window
+ // unfocused while whatever was focused *before* the switch (now
+ // invisible, off on the old workspace) kept receiving real
+ // keyboard input.
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ let ws2 = wm.add_workspace("2", "dynamic");
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+ wm.move_window_to_workspace(b, ws2);
+ wm.focus_window(a);
+ assert_eq!(wm.focused_id(), Some(a), "sanity: a is focused on the original workspace");
+
+ wm.switch_workspace(ws2);
+ assert_eq!(wm.focused_id(), Some(b), "switching to a workspace with a window must focus it, not leave the old workspace's window focused");
+ }
+
+ #[test]
+ fn switching_to_an_empty_workspace_clears_focus() {
+ let mut wm = wm_with_monitor();
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ wm.focus_window(a);
+ let empty_ws = wm.add_workspace("2", "dynamic");
+
+ wm.switch_workspace(empty_ws);
+ assert_eq!(wm.focused_id(), None, "no window on the new workspace to focus, and the old one is no longer visible");
+ }
+
+ #[test]
+ fn per_monitor_workspaces_off_by_default_switch_workspace_still_moves_every_monitor() {
+ // Sanity: the new `per_monitor_workspaces` field must default to
+ // `false` and leave shared-mode behaviour completely unchanged --
+ // every existing workspace test above this one relies on that.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ assert!(!wm.per_monitor_workspaces, "shared mode must be the default");
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace(ws2);
+ assert_eq!(wm.workspace_for_monitor(0), ws2);
+ assert_eq!(wm.workspace_for_monitor(1), ws2, "shared mode: every monitor must agree");
+ }
+
+ #[test]
+ fn per_monitor_workspaces_on_switching_one_monitor_leaves_the_other_alone() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.per_monitor_workspaces = true;
+ let ws2 = wm.add_workspace("2", "dynamic");
+
+ wm.switch_workspace_on_monitor(ws2, 1);
+
+ assert_eq!(wm.workspace_for_monitor(1), ws2, "monitor 1 switched");
+ assert_eq!(wm.workspace_for_monitor(0), 1, "monitor 0 must still fall back to current_workspace, untouched");
+ }
+
+ #[test]
+ fn per_monitor_workspaces_on_visible_windows_respects_each_monitors_own_workspace() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.per_monitor_workspaces = true;
+ let ws2 = wm.add_workspace("2", "dynamic");
+
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "on-monitor-0-workspace-1"));
+ wm.window_mut(a).unwrap().monitor = 0;
+
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "on-monitor-1-workspace-2"));
+ wm.window_mut(b).unwrap().monitor = 1;
+ wm.move_window_to_workspace(b, ws2);
+
+ // Before switching monitor 1 to workspace 2, b isn't visible yet
+ // (monitor 1 still falls back to workspace 1).
+ assert!(!wm.visible_windows().any(|w| w.id == b));
+
+ wm.switch_workspace_on_monitor(ws2, 1);
+
+ let visible: Vec<_> = wm.visible_windows().map(|w| w.id).collect();
+ assert!(visible.contains(&a), "monitor 0's own window must still be visible");
+ assert!(visible.contains(&b), "monitor 1's window must become visible once its monitor switches to workspace 2");
+ }
+
+ #[test]
+ fn per_monitor_workspaces_on_multiple_workspaces_can_be_active_at_once() {
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.per_monitor_workspaces = true;
+ let ws2 = wm.add_workspace("2", "dynamic");
+
+ wm.switch_workspace_on_monitor(ws2, 1);
+
+ assert!(wm.is_workspace_visible(1), "monitor 0 is still showing workspace 1");
+ assert!(wm.is_workspace_visible(ws2), "monitor 1 is showing workspace 2");
+ }
+
+ #[test]
+ fn switching_to_a_workspace_where_the_already_focused_window_lives_is_a_no_op_for_focus() {
+ // The auto-focus-on-switch behavior above must not fight
+ // `focus_window`'s own workspace-follow call into `switch_workspace`
+ // (see that function's doc comment): when a window on another
+ // workspace is focused directly, that window - not merely "the
+ // topmost window on its workspace" - must end up focused, even if
+ // it isn't the topmost one.
+ let mut wm = wm_with_monitor();
+ let ws2 = wm.add_workspace("2", "dynamic");
+ let a = wm.alloc_window_id();
+ wm.add_window(Window::new(a, "a"));
+ wm.move_window_to_workspace(a, ws2);
+ let b = wm.alloc_window_id();
+ wm.add_window(Window::new(b, "b"));
+ wm.move_window_to_workspace(b, ws2);
+ // b was added after a, so it's topmost - focusing a directly must
+ // still result in a being focused, not b.
+ wm.focus_window(a);
+ assert_eq!(wm.focused_id(), Some(a));
+ }
+
+ #[test]
fn focusing_a_window_on_another_workspace_switches_to_it() {
// Regression test: `focus_window` used to mark the target focused
// without ever touching `current_workspace` - reported live
@@ -421,7 +676,7 @@
let id = wm.alloc_window_id();
wm.add_window(Window::new(id, "a"));
wm.move_window_to_workspace(id, ws2);
- assert_eq!(wm.current_workspace(), 0, "sanity: still on the default workspace");
+ assert_eq!(wm.current_workspace(), 1, "sanity: still on the default workspace");
wm.focus_window(id);
assert_eq!(wm.current_workspace(), ws2, "focusing a window must bring its workspace along");
@@ -482,7 +737,7 @@
// workspace id that was never really visited.
wm.auto_back_and_forth = true;
wm.switch_workspace(ws2);
- assert_eq!(wm.current_workspace(), 0);
+ assert_eq!(wm.current_workspace(), 1);
}
#[test]
@@ -642,6 +897,75 @@
}
#[test]
+ fn disabled_monitor_is_reported_but_never_shows_up_in_monitors() {
+ // The whole point of keeping this separate from `set_monitors`:
+ // real placement (`monitors()`) must never see a disabled output,
+ // even though `srd monitors`/AGS's panel now needs to list it.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.set_disabled_monitor("HDMI-A-1".to_string(), Rect::new(1920, 0, 1920, 1080), Rect::new(1920, 0, 1920, 1080), false);
+
+ assert_eq!(wm.monitors().len(), 2, "disabled_monitors must not leak into real placement's monitor list");
+ let disabled: Vec<_> = wm.disabled_monitors().collect();
+ assert_eq!(disabled.len(), 1);
+ assert_eq!(disabled[0].0, "HDMI-A-1");
+ }
+
+ #[test]
+ fn re_enabling_clears_the_disabled_monitor_record() {
+ let mut wm = WindowManager::new();
+ wm.set_disabled_monitor("HDMI-A-1".to_string(), Rect::new(0, 0, 1920, 1080), Rect::new(0, 0, 1920, 1080), false);
+ assert_eq!(wm.disabled_monitors().count(), 1);
+
+ wm.clear_disabled_monitor("HDMI-A-1");
+ assert_eq!(wm.disabled_monitors().count(), 0);
+ }
+
+ #[test]
+ fn primary_secondary_layout_is_a_no_op_outside_per_monitor_workspaces_mode() {
+ // Shared mode: every monitor shows the same one workspace, so a
+ // primary/secondary split has nothing distinct to apply to.
+ let mut wm = WindowManager::new();
+ wm.primary_layout = "dynamic".to_string();
+ wm.secondary_layout = "tiling".to_string();
+ wm.set_monitors(two_monitors());
+ assert_eq!(wm.workspace(1).unwrap().layout, "dynamic", "must not touch the shared workspace's layout");
+ }
+
+ #[test]
+ fn primary_secondary_layout_applies_once_workspaces_are_split_per_monitor() {
+ let mut wm = WindowManager::new();
+ wm.per_monitor_workspaces = true;
+ wm.primary_layout = "dynamic".to_string();
+ wm.secondary_layout = "tiling".to_string();
+ wm.set_monitors(two_monitors());
+ // Give the secondary monitor its own workspace, same as a real
+ // independent per-monitor switch would.
+ let ws2 = wm.add_workspace("2", "dynamic");
+ wm.switch_workspace_on_monitor(ws2, 1);
+ // Re-applied on the next monitor-list refresh (a hotplug or
+ // restart), not continuously - see `apply_monitor_layouts`'s own
+ // doc comment for why it doesn't hook every workspace switch.
+ wm.set_monitors(two_monitors());
+
+ assert_eq!(wm.workspace(wm.workspace_for_monitor(0)).unwrap().layout, "dynamic");
+ assert_eq!(wm.workspace(ws2).unwrap().layout, "tiling");
+ }
+
+ #[test]
+ fn primary_secondary_layout_does_not_clobber_the_still_shared_workspace() {
+ // Neither monitor has been independently switched yet - both
+ // still resolve to the same fallback workspace. secondary_layout
+ // must not stomp what primary_layout just set on it.
+ let mut wm = WindowManager::new();
+ wm.per_monitor_workspaces = true;
+ wm.primary_layout = "dynamic".to_string();
+ wm.secondary_layout = "tiling".to_string();
+ wm.set_monitors(two_monitors());
+ assert_eq!(wm.workspace(1).unwrap().layout, "dynamic");
+ }
+
+ #[test]
fn unplugging_a_monitor_rehomes_its_windows_to_the_primary() {
let mut wm = WindowManager::new();
wm.set_monitors(two_monitors());
@@ -749,6 +1073,69 @@
);
}
+ #[test]
+ fn a_new_window_lands_on_the_focused_windows_monitor_not_always_primary() {
+ // Real bug, reported live: "why do all windows only open on the
+ // first monitor" - `add_window` used to resolve its target
+ // monitor via `primary_monitor()` unconditionally, so a second
+ // monitor being the one the user was actually working on never
+ // mattered at all.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+
+ let first = wm.alloc_window_id();
+ wm.add_window(Window::new(first, "on-primary"));
+ assert_eq!(wm.window(first).unwrap().monitor, 0, "sanity: nothing focused yet falls back to primary");
+
+ // `add_window` itself focuses whatever it just added, so moving
+ // this window onto the secondary monitor and leaving it focused is
+ // enough to make it "the window the user is currently on" for the
+ // next one.
+ wm.window_mut(first).unwrap().monitor = 1;
+
+ let second = wm.alloc_window_id();
+ wm.add_window(Window::new(second, "should-follow-focus"));
+ assert_eq!(wm.window(second).unwrap().monitor, 1, "a new window must land on the focused window's monitor, not primary");
+ }
+
+ #[test]
+ fn a_new_window_lands_on_the_pointers_monitor_when_nothing_is_focused_there() {
+ // Real bug, reported live: with nothing focused (a fresh session,
+ // or the last-focused window sitting on a *different* monitor than
+ // the one just clicked/hovered), a new window still fell all the
+ // way back to primary - even though the user was demonstrably at
+ // the second monitor when they launched it. `set_pointer_monitor`
+ // is what a real backend's pointer-motion handler calls to tell
+ // core this.
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+ wm.set_pointer_monitor(Some(1));
+
+ let id = wm.alloc_window_id();
+ wm.add_window(Window::new(id, "should-follow-pointer"));
+ assert_eq!(wm.window(id).unwrap().monitor, 1, "a new window must land on the pointer's monitor when nothing is focused, not primary");
+ }
+
+ #[test]
+ fn a_focused_window_still_wins_over_the_pointers_monitor() {
+ // The pointer is only a fallback for when nothing is focused --
+ // see `add_window`'s own doc comment for why focus stays the
+ // primary signal (matches every mainstream desktop's "new window
+ // opens where you're working" convention, which is about the
+ // focused context, not incidental cursor position).
+ let mut wm = WindowManager::new();
+ wm.set_monitors(two_monitors());
+
+ let first = wm.alloc_window_id();
+ wm.add_window(Window::new(first, "focused-on-primary"));
+ wm.window_mut(first).unwrap().monitor = 0;
+ wm.set_pointer_monitor(Some(1));
+
+ let second = wm.alloc_window_id();
+ wm.add_window(Window::new(second, "should-still-follow-focus"));
+ assert_eq!(wm.window(second).unwrap().monitor, 0, "a focused window's monitor must win over the pointer's");
+ }
+
// ---- Fullscreen ------------------------------------------------------
#[test]