1package core
2
3import (
4 "reflect"
5 "slices"
6 "testing"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11func monitor() *Monitor {
12 m := NewMonitor()
13 m.SetOutput(100, 80)
14 m.SetMaxColumns(2)
15 return m
16}
17
18// windows lists each workspace's windows, in column order.
19func windows(m *Monitor) [][]WindowID {
20 var out [][]WindowID
21 for _, w := range m.Workspaces {
22 list := []WindowID{}
23 for _, c := range w.Columns {
24 list = append(list, c.Windows...)
25 }
26 out = append(out, list)
27 }
28 return out
29}
30
31func TestMonitorKeepsOneEmptyWorkspaceBelow(t *testing.T) {
32 m := monitor()
33 if len(m.Workspaces) != 1 || m.Active != 0 {
34 t.Fatal(windows(m))
35 }
36 m.AddWindow(1)
37 if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {}}) {
38 t.Fatal(windows(m))
39 }
40 // Cmd+9 past the end lands on the trailing empty workspace.
41 m.FocusNumber(9)
42 if m.Active != 1 {
43 t.Fatal(m.Active)
44 }
45 m.AddWindow(2)
46 if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {2}, {}}) {
47 t.Fatal(windows(m))
48 }
49}
50
51func TestMonitorRemovesEmptyWorkspaceOnceLeft(t *testing.T) {
52 m := monitor()
53 m.AddWindow(1)
54 m.FocusNumber(2)
55 m.AddWindow(2)
56 m.FocusNumber(3)
57 m.AddWindow(3)
58 // [1:{1}] [2:{2}] [3:{3}] [4:{}]
59 m.FocusNumber(2)
60 m.RemoveWindow(2)
61 // Still on the now-empty workspace 2: it stays while active.
62 if len(m.Workspaces) != 4 || m.Active != 1 {
63 t.Fatal(windows(m), m.Active)
64 }
65 m.FocusNumber(1)
66 // Once left it is removed: workspace 3 becomes 2.
67 if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {3}, {}}) || m.Active != 0 {
68 t.Fatal(windows(m), m.Active)
69 }
70 m.FocusNumber(2)
71 if id, _ := m.Focused(); id != 3 {
72 t.Fatal(id)
73 }
74}
75
76func TestMonitorFirstWorkspaceStays(t *testing.T) {
77 m := monitor()
78 m.AddWindow(1)
79 m.RemoveWindow(1)
80 m.FocusNumber(2)
81 if len(m.Workspaces) != 1 || m.Active != 0 {
82 t.Fatal(windows(m), m.Active)
83 }
84 // Leaving an empty first workspace does not remove it.
85 m.AddWindow(1)
86 m.FocusNumber(2)
87 m.AddWindow(2)
88 m.FocusNumber(1)
89 m.RemoveWindow(1)
90 m.FocusNumber(2)
91 if !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {}}) {
92 t.Fatal(windows(m))
93 }
94}
95
96func TestMonitorRemoveElsewhereKeepsActive(t *testing.T) {
97 m := monitor()
98 m.AddWindow(1)
99 m.FocusNumber(2)
100 m.AddWindow(2)
101 m.FocusNumber(3)
102 m.AddWindow(3)
103 m.FocusNumber(3)
104 // A window on another workspace closes: the user stays on workspace 3's windows.
105 m.RemoveWindow(2)
106 if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {3}, {}}) || m.Active != 1 {
107 t.Fatal(windows(m), m.Active)
108 }
109 if id, _ := m.Focused(); id != 3 {
110 t.Fatal(id)
111 }
112}
113
114func TestMonitorLayoutHidesOtherWorkspaces(t *testing.T) {
115 m := monitor()
116 m.AddWindow(1)
117 m.FocusNumber(2)
118 m.AddWindow(2)
119 for _, p := range m.Layout() {
120 if hidden := p.ID == 1; p.Hidden != hidden {
121 t.Fatal(m.Layout())
122 }
123 }
124}
125
126func TestMonitorFullscreenRequestStaysPut(t *testing.T) {
127 m := monitor()
128 m.AddWindow(1)
129 m.FocusNumber(2)
130 m.AddWindow(2)
131 m.SetFullscreen(1, true)
132 if m.Active != 1 {
133 t.Fatal("client request switched workspace")
134 }
135 if m.Workspaces[0].fullscreen != 1 {
136 t.Fatal("fullscreen not applied")
137 }
138}
139
140func TestMonitorActions(t *testing.T) {
141 m := monitor()
142 m.AddWindow(1)
143 m.AddWindow(2)
144 // Down past the only window of the column goes to the workspace below.
145 m.Apply(ActionFocusWindowDown)
146 if m.Active != 1 {
147 t.Fatal(m.Active)
148 }
149 m.Apply(ActionFocusWindowUp)
150 if m.Active != 0 {
151 t.Fatal(m.Active)
152 }
153 // Up at the top workspace stays.
154 m.Apply(ActionFocusWorkspaceUp)
155 if m.Active != 0 {
156 t.Fatal(m.Active)
157 }
158 // Move the focused window (2) down: focus stays on workspace 1.
159 m.Apply(ActionMoveWindowToWorkspaceDown)
160 if !reflect.DeepEqual(windows(m), [][]WindowID{{1}, {2}, {}}) || m.Active != 0 {
161 t.Fatal(windows(m), m.Active)
162 }
163 m.Apply("move-window-to-workspace 3")
164 if !reflect.DeepEqual(windows(m), [][]WindowID{{}, {2}, {1}, {}}) || m.Active != 0 {
165 t.Fatal(windows(m), m.Active)
166 }
167 m.Apply("focus-workspace 3")
168 if id, _ := m.Focused(); id != 1 {
169 t.Fatal(id)
170 }
171 // Workspace 1 is empty but first: it stays.
172 if len(m.Workspaces) != 4 {
173 t.Fatal(windows(m))
174 }
175 if e := m.Apply("spawn foot"); !e.Spawn {
176 t.Fatal(e)
177 }
178}
179
180func TestEqualSharesAndBorderless(t *testing.T) {
181 m := monitor()
182 m.SetMaxColumns(3)
183 m.AddWindow(1)
184 p := m.Layout()
185 if p[0].Rect.W != 100 || p[0].Neighbors != 0 {
186 t.Fatal(p)
187 }
188 m.AddWindow(2)
189 m.AddWindow(3)
190 p = m.Layout()
191 if p[0].Rect.W != 33 || p[1].Rect.W != 33 || p[2].Rect.W != 33 || p[0].Neighbors != ports.SideRight || p[0].Inset != ports.SideRight {
192 t.Fatal(p)
193 }
194 // A fourth column scrolls: still 1/3 each.
195 m.AddWindow(4)
196 if w := m.Current(); w.columnWidth(3) != 33 || w.ViewX == 0 {
197 t.Fatal(w.ViewX)
198 }
199 // A preset on one column is kept; auto columns keep their share.
200 m.SetPresets([]Width{{Num: 1, Den: 2}})
201 m.Apply(ActionCycleColumnWidth)
202 if w := m.Current(); w.columnWidth(w.Focus) != 50 || w.columnWidth(0) != 33 {
203 t.Fatal(w.columnWidth(w.Focus), w.columnWidth(0))
204 }
205}
206
207// stack returns a monitor with columns [1] [2 3], focus on window 3.
208func stack() *Monitor {
209 m := monitor()
210 m.AddWindow(1)
211 m.AddWindow(2)
212 m.AddWindow(3)
213 w := m.Current()
214 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3}, Focus: 1}}
215 w.Focus = 1
216 return m
217}
218
219func TestMoveColumnToWorkspace(t *testing.T) {
220 t.Run("whole column, focus stays", func(t *testing.T) {
221 m := stack()
222 m.Apply("move-column-to-workspace 2")
223 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2, 3}, {}}) || m.Active != 0 {
224 t.Fatal(got, m.Active)
225 }
226 if w := m.Workspaces[1]; len(w.Columns) != 1 || w.Columns[0].Focus != 1 {
227 t.Fatal(w.Columns)
228 }
229 if id, _ := m.Focused(); id != 1 {
230 t.Fatal(id)
231 }
232 })
233 t.Run("window only", func(t *testing.T) {
234 m := stack()
235 m.Apply(ActionMoveWindowToWorkspaceDown)
236 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {3}, {}}) {
237 t.Fatal(got)
238 }
239 })
240 t.Run("follow shows the target", func(t *testing.T) {
241 m := stack()
242 m.SetFollowMove(true)
243 m.Apply(ActionMoveColumnToWorkspaceDown)
244 if id, _ := m.Focused(); m.Active != 1 || id != 3 {
245 t.Fatal(m.Active, id)
246 }
247 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1}, {2, 3}, {}}) {
248 t.Fatal(got)
249 }
250 })
251 t.Run("the slot window alone releases its slot", func(t *testing.T) {
252 m := stack()
253 c := &m.Current().Columns[1]
254 c.Slot, c.Focus = 2, 0
255 m.Apply(ActionMoveWindowToWorkspaceDown)
256 if c := m.Current().Columns[1]; c.Slot != 0 || c.Windows[0] != 3 {
257 t.Fatal(c)
258 }
259 })
260 t.Run("follow a floating window", func(t *testing.T) {
261 m := stack()
262 m.SetFollowMove(true)
263 m.AddFloating(9, 10, 10)
264 m.Current().FocusID(9)
265 m.Apply(ActionMoveWindowToWorkspaceDown)
266 if id, _ := m.Focused(); m.Active != 1 || id != 9 {
267 t.Fatal(m.Active, id)
268 }
269 })
270 t.Run("slot column loses its slot", func(t *testing.T) {
271 m := stack()
272 m.Current().Columns[1].Slot = 2
273 m.Apply(ActionMoveColumnToWorkspaceDown)
274 if c := m.Workspaces[1].Columns[0]; c.Slot != 0 {
275 t.Fatal(c)
276 }
277 })
278}
279
280func TestWorkspaceArg(t *testing.T) {
281 for _, tc := range []struct {
282 a Action
283 n int
284 op WorkspaceOp
285 ok bool
286 }{{"focus-workspace 3", 3, FocusWorkspace, true}, {"move-column-to-workspace 12", 12, MoveColumnToWorkspace, true}, {"move-window-to-workspace 2", 2, MoveWindowToWorkspace, true}, {"move-to-workspace 2", 0, 0, false}, {"focus-workspace 0", 0, 0, false}, {"focus-workspace x", 0, 0, false}, {"quit", 0, 0, false}} {
287 n, op, ok := WorkspaceArg(tc.a)
288 if n != tc.n || op != tc.op || ok != tc.ok {
289 t.Errorf("%s: %d %v %v", tc.a, n, op, ok)
290 }
291 }
292}
293
294func TestFullscreenRequestKeepsFocus(t *testing.T) {
295 m := monitor()
296 m.AddWindow(1)
297 m.AddWindow(2)
298 m.SetFullscreen(1, true)
299 if id, _ := m.Focused(); id != 2 {
300 t.Fatal(id)
301 }
302 if m.Current().fullscreen != 1 {
303 t.Fatal("not fullscreen")
304 }
305}
306
307func TestLonePresetColumnKeepsWidth(t *testing.T) {
308 m := monitor()
309 m.SetPresets([]Width{{Num: 1, Den: 2}})
310 m.AddWindow(1)
311 m.Apply(ActionCycleColumnWidth)
312 if p := m.Layout()[0]; p.Rect.W != 50 || p.Neighbors != 0 {
313 t.Fatal(p)
314 }
315}
316
317func named(m *Monitor, specs ...NamedWorkspace) *Monitor {
318 m.SetNamed(specs)
319 return m
320}
321
322func TestHiddenWorkspaceToggle(t *testing.T) {
323 m := named(monitor(), NamedWorkspace{Name: "dev", Hidden: true})
324 m.AddWindow(1)
325 if len(m.Workspaces) != 2 {
326 t.Fatal("hidden workspace is numbered", windows(m))
327 }
328 m.Apply("workspace dev")
329 if m.Current().Name != "dev" {
330 t.Fatal(m.Current().Name)
331 }
332 m.AddWindow(2)
333 // Up/down do not leave a hidden workspace.
334 m.Apply(ActionFocusWorkspaceDown)
335 m.Apply(ActionFocusWindowDown)
336 if m.Current().Name != "dev" {
337 t.Fatal("left the hidden workspace")
338 }
339 for _, p := range m.Layout() {
340 if hidden := p.ID == 1; p.Hidden != hidden {
341 t.Fatal(m.Layout())
342 }
343 }
344 // The same bind returns to the previous workspace.
345 m.Apply("workspace dev")
346 if id, _ := m.Focused(); id != 1 || m.Current().Name != "" {
347 t.Fatal(id, m.Current().Name)
348 }
349 // Cmd+N from the hidden workspace goes to that number.
350 m.Apply("workspace dev")
351 m.FocusNumber(1)
352 if id, _ := m.Focused(); id != 1 {
353 t.Fatal(id)
354 }
355 // Moving a window up/down from a hidden workspace does nothing.
356 m.Apply("workspace dev")
357 m.Apply(ActionMoveWindowToWorkspaceDown)
358 m.Apply(ActionMoveWindowToWorkspaceUp)
359 if id, _ := m.Focused(); id != 2 || m.Current().Name != "dev" {
360 t.Fatal(id, windows(m))
361 }
362}
363
364func TestNamedWorkspaceIsNumberedAndKept(t *testing.T) {
365 m := monitor()
366 m.AddWindow(1)
367 named(m, NamedWorkspace{Name: "web"})
368 // Inserted above the trailing empty workspace: [1:{1}] [2:web] [3:{}].
369 if len(m.Workspaces) != 3 || m.Workspaces[1].Name != "web" || m.Active != 0 {
370 t.Fatal(windows(m), m.Active)
371 }
372 // Empty named workspaces are not removed when left.
373 m.FocusNumber(2)
374 m.FocusNumber(1)
375 if m.Workspaces[1].Name != "web" {
376 t.Fatal(windows(m))
377 }
378 m.Apply("workspace web")
379 if m.Active != 1 {
380 t.Fatal(m.Active)
381 }
382 m.Apply("workspace web")
383 if m.Active != 0 {
384 t.Fatal("toggle did not return", m.Active)
385 }
386 m.Apply("workspace nope") // unknown: nothing
387 if m.Active != 0 {
388 t.Fatal(m.Active)
389 }
390}
391
392func TestNamedWorkspaceReload(t *testing.T) {
393 m := named(monitor(), NamedWorkspace{Name: "dev", Hidden: true}, NamedWorkspace{Name: "web"})
394 m.Apply("workspace dev")
395 m.AddWindow(7)
396 // Reload keeps windows and the workspace on screen; web becomes hidden.
397 named(m, NamedWorkspace{Name: "dev", Hidden: true}, NamedWorkspace{Name: "web", Hidden: true})
398 if m.Current().Name != "dev" || len(m.Workspaces) != 1 {
399 t.Fatal(m.Current().Name, windows(m))
400 }
401 // Dropping dev from config hands its windows to the numbered workspace.
402 named(m)
403 if m.Current().Name != "" || !reflect.DeepEqual(windows(m), [][]WindowID{{7}, {}}) {
404 t.Fatal(m.Current().Name, windows(m))
405 }
406}
407
408func TestNamedWorkspaceSettings(t *testing.T) {
409 m := named(monitor(), NamedWorkspace{Name: "dev", Hidden: true, MaxColumns: 3, Overflow: OverflowFixed})
410 m.SetMaxColumns(1)
411 if m.Current().MaxColumns != 1 || m.Current().Overflow != "" {
412 t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
413 }
414 m.Apply("workspace dev")
415 if m.Current().MaxColumns != 3 || m.Current().Overflow != OverflowFixed {
416 t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
417 }
418 // New dynamic workspaces follow the defaults.
419 m.SetOverflow(OverflowScroll)
420 m.Apply("workspace dev")
421 m.FocusNumber(9)
422 if m.Current().MaxColumns != 1 || m.Current().Overflow != OverflowScroll {
423 t.Fatal(m.Current().MaxColumns, m.Current().Overflow)
424 }
425}
426
427// checkInvariants fails when the monitor is in a state no action should reach.
428func checkInvariants(t *testing.T, m *Monitor, want []WindowID) {
429 t.Helper()
430 if m.Active < 0 || m.Active >= len(m.Workspaces) {
431 t.Fatalf("active %d of %d", m.Active, len(m.Workspaces))
432 }
433 if last := m.Workspaces[len(m.Workspaces)-1]; !last.empty() || last.Name != "" {
434 t.Fatal("no trailing empty unnamed workspace")
435 }
436 if m.shown != nil && indexOf(m.hidden, m.shown) < 0 {
437 t.Fatal("shown is not a hidden workspace")
438 }
439 if m.back != nil && !m.has(m.back) {
440 t.Fatal("back dangles")
441 }
442 seen := map[WindowID]int{}
443 for _, w := range append(append([]*Workspace(nil), m.Workspaces...), m.hidden...) {
444 for _, c := range w.Columns {
445 for _, id := range c.Windows {
446 seen[id]++
447 }
448 }
449 }
450 for _, id := range want {
451 if seen[id] != 1 {
452 t.Fatalf("window %d present %d times", id, seen[id])
453 }
454 }
455 if len(seen) != len(want) {
456 t.Fatalf("windows %v, want %v", seen, want)
457 }
458}
459
460func TestSetNamedTransitions(t *testing.T) {
461 dev := NamedWorkspace{Name: "dev"}
462 devHidden := NamedWorkspace{Name: "dev", Hidden: true}
463 web := NamedWorkspace{Name: "web"}
464 for _, tc := range []struct {
465 name string
466 before []NamedWorkspace
467 after []NamedWorkspace
468 onDev bool // show dev before the reload
469 want string // workspace name on screen after
470 }{
471 {"numbered active becomes hidden", []NamedWorkspace{dev}, []NamedWorkspace{devHidden}, true, "dev"},
472 {"hidden shown becomes numbered", []NamedWorkspace{devHidden}, []NamedWorkspace{dev}, true, "dev"},
473 {"rename drops the old name", []NamedWorkspace{dev}, []NamedWorkspace{web}, true, ""},
474 {"hidden shown is removed", []NamedWorkspace{devHidden}, nil, true, ""},
475 {"hidden not shown is removed", []NamedWorkspace{devHidden, web}, []NamedWorkspace{web}, false, ""},
476 {"add while on numbered", nil, []NamedWorkspace{devHidden, web}, false, ""},
477 } {
478 t.Run(tc.name, func(t *testing.T) {
479 m := named(monitor(), tc.before...)
480 m.AddWindow(1)
481 if tc.onDev {
482 m.Apply("workspace dev")
483 }
484 m.AddWindow(2)
485 checkInvariants(t, m, []WindowID{1, 2})
486 m.SetNamed(tc.after)
487 checkInvariants(t, m, []WindowID{1, 2})
488 if got := m.Current().Name; got != tc.want {
489 t.Fatalf("on %q, want %q", got, tc.want)
490 }
491 if id, _ := m.Focused(); id != 2 {
492 t.Fatalf("focus moved to %d", id)
493 }
494 })
495 }
496}
497
498// Under fixed overflow, fullscreen moves the window to its own workspace
499// below; workspace up/down still work and leaving brings it back in place.
500func TestMonitorFixedFullscreenOwnWorkspace(t *testing.T) {
501 m := monitor()
502 m.SetOverflow(OverflowFixed)
503 for id := WindowID(1); id <= 3; id++ {
504 m.AddWindow(id)
505 }
506 m.Current().FocusID(2)
507 m.ToggleFullscreen()
508 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 3}, {2}, {}}) || m.Active != 1 {
509 t.Fatal(got, m.Active)
510 }
511 if p := m.Layout(); !slices.ContainsFunc(p, func(p Placement) bool { return p.ID == 2 && p.Fullscreen && !p.Hidden }) {
512 t.Fatal(p)
513 }
514 m.Apply(ActionFocusWorkspaceUp)
515 if m.Active != 0 {
516 t.Fatal("stuck on the fullscreen workspace")
517 }
518 m.Apply(ActionFocusWorkspaceDown)
519 m.ToggleFullscreen()
520 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3}, {}}) || m.Active != 0 {
521 t.Fatal(got, m.Active)
522 }
523 if id, _ := m.Focused(); id != 2 {
524 t.Fatal("focus", id)
525 }
526
527 // A client request moves it too, without taking the user along
528 // unless the window is the focused one on screen.
529 m.Current().FocusID(1)
530 m.SetFullscreen(3, true)
531 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {3}, {}}) || m.Active != 0 {
532 t.Fatal(got, m.Active)
533 }
534 m.SetFullscreen(3, false)
535 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3}, {}}) {
536 t.Fatal(got)
537 }
538
539 // Closing the fullscreen window drops its workspace and returns.
540 m.Current().FocusID(3)
541 m.ToggleFullscreen()
542 m.RemoveWindow(3)
543 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) || m.Active != 0 {
544 t.Fatal(got, m.Active)
545 }
546
547 // Scroll overflow keeps the niri behaviour: same workspace.
548 m.SetOverflow(OverflowScroll)
549 m.ToggleFullscreen()
550 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2}, {}}) {
551 t.Fatal(got)
552 }
553}
554
555// The fullscreen workspace link survives only while it holds just its
556// window; exits restore the exact place and never move the user's focus.
557func TestMonitorFixedFullscreenEdges(t *testing.T) {
558 fixed := func() *Monitor {
559 m := monitor()
560 m.SetOverflow(OverflowFixed)
561 for id := WindowID(1); id <= 3; id++ {
562 m.AddWindow(id)
563 }
564 return m
565 }
566 t.Run("a window opened there floats, then tiles after it at home", func(t *testing.T) {
567 m := fixed()
568 m.Current().FocusID(2)
569 m.ToggleFullscreen()
570 m.AddWindow(4)
571 if fs := m.Current(); fs.floatIndex(4) != 0 || fs.fullscreen != 2 {
572 t.Fatal(fs.Floats, fs.fullscreen)
573 }
574 m.SetFullscreen(2, false)
575 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 4, 3}, {}}) || m.Active != 0 {
576 t.Fatal(got, m.Active)
577 }
578 })
579 t.Run("a joined window lands after its stack when columns shifted", func(t *testing.T) {
580 m := fixed()
581 w := m.Current()
582 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{5}}, {Windows: []WindowID{2, 3}}, {Windows: []WindowID{6}}}
583 w.FocusID(3)
584 m.ToggleFullscreen()
585 m.AddWindow(7)
586 m.RemoveWindow(1)
587 m.ToggleFullscreen()
588 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{5, 2, 3, 7, 6}, {}}) {
589 t.Fatal(got)
590 }
591 })
592 t.Run("the bind from a dialog returns the fullscreen window", func(t *testing.T) {
593 m := fixed()
594 m.Current().FocusID(2)
595 m.ToggleFullscreen()
596 m.AddFloating(4, 10, 10) // focused dialog
597 m.ToggleFullscreen()
598 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3}, {}}) || m.Current().floatIndex(4) != 0 {
599 t.Fatal(got, m.Current().Floats)
600 }
601 })
602 t.Run("a dialog asking fullscreen keeps the link", func(t *testing.T) {
603 m := fixed()
604 m.Current().FocusID(2)
605 m.ToggleFullscreen()
606 m.AddFloating(4, 10, 10)
607 m.SetFullscreen(4, true)
608 m.SetFullscreen(4, false)
609 m.ToggleFullscreen()
610 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3}, {}}) {
611 t.Fatal(got)
612 }
613 })
614 t.Run("moved in by hand it floats", func(t *testing.T) {
615 m := fixed()
616 m.Current().FocusID(2)
617 m.ToggleFullscreen()
618 m.Apply(ActionFocusWorkspaceUp)
619 m.Current().FocusID(1)
620 m.Apply(ActionMoveWindowToWorkspaceDown)
621 if fs := m.Workspaces[1]; fs.floatIndex(1) != 0 || fs.origin == nil {
622 t.Fatal(fs.Floats)
623 }
624 })
625 t.Run("a moved column floats window by window", func(t *testing.T) {
626 m := fixed()
627 m.Current().FocusID(2)
628 m.ToggleFullscreen()
629 m.Apply(ActionFocusWorkspaceUp)
630 w := m.Current()
631 ids := w.windows()
632 w.Columns = []Column{{Windows: ids}}
633 w.Focus = 0
634 m.Apply(ActionMoveColumnToWorkspaceDown)
635 fs := m.Workspaces[1]
636 for _, id := range ids {
637 if fs.floatIndex(id) < 0 {
638 t.Fatal(id, fs.Floats)
639 }
640 }
641 if fs.origin == nil || len(ids) < 2 {
642 t.Fatal(fs.origin, ids)
643 }
644 })
645 t.Run("closing it brings its dialogs home, below a focused float", func(t *testing.T) {
646 m := fixed()
647 m.Current().FocusID(2)
648 m.ToggleFullscreen()
649 m.AddFloating(4, 10, 10)
650 m.Apply(ActionFocusWorkspaceUp)
651 m.AddFloating(9, 10, 10)
652 m.RemoveWindow(2)
653 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 3}, {}}) || m.Current().floatIndex(4) != 0 {
654 t.Fatal(got, m.Current().Floats)
655 }
656 if id, _ := m.Focused(); id != 9 {
657 t.Fatal("focus", id)
658 }
659 })
660 t.Run("stack of three keeps its column", func(t *testing.T) {
661 m := fixed()
662 w := m.Current()
663 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3, 5}}}
664 w.FocusID(3)
665 m.ToggleFullscreen()
666 m.RemoveWindow(2)
667 m.ToggleFullscreen()
668 if len(w.Columns) != 2 || !slices.Equal(w.Columns[1].Windows, []WindowID{3, 5}) {
669 t.Fatal(w.Columns)
670 }
671 })
672 t.Run("client exit keeps the user's focus", func(t *testing.T) {
673 m := fixed()
674 m.Current().FocusID(1)
675 m.SetFullscreen(3, true)
676 m.SetFullscreen(3, false)
677 if id, _ := m.Focused(); id != 1 || m.Active != 0 {
678 t.Fatal("focus moved to", id, m.Active)
679 }
680 m.Current().FocusID(3)
681 m.SetFullscreen(1, true)
682 m.SetFullscreen(1, false)
683 if id, _ := m.Focused(); id != 3 {
684 t.Fatal("focus moved to", id)
685 }
686 })
687 t.Run("stacked slot window returns to its row", func(t *testing.T) {
688 m := fixed()
689 w := m.Current()
690 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3}, Slot: 2}}
691 w.FocusID(3)
692 m.ToggleFullscreen()
693 m.ToggleFullscreen()
694 if !reflect.DeepEqual(w.Columns[1], Column{Windows: []WindowID{2, 3}, Focus: 1, Slot: 2}) || w.Focus != 1 {
695 t.Fatal(w.Columns, w.Focus)
696 }
697 })
698 t.Run("moved out by hand unlinks it", func(t *testing.T) {
699 m := fixed()
700 m.Current().FocusID(2)
701 m.ToggleFullscreen()
702 m.Apply(ActionMoveWindowToWorkspaceUp)
703 m.AddWindow(4)
704 m.RemoveWindow(4)
705 if m.Active != 1 {
706 t.Fatal("view jumped to", m.Active)
707 }
708 })
709 t.Run("client exit on screen keeps the window focused", func(t *testing.T) {
710 m := fixed()
711 m.Current().FocusID(1)
712 m.ToggleFullscreen()
713 m.SetFullscreen(1, false)
714 if id, _ := m.Focused(); id != 1 || m.Active != 0 {
715 t.Fatal("focus", id, m.Active)
716 }
717 })
718 t.Run("stack gone: back as a column", func(t *testing.T) {
719 m := fixed()
720 w := m.Current()
721 w.Columns = []Column{{Windows: []WindowID{1}}, {Windows: []WindowID{2, 3}}, {Windows: []WindowID{4}}}
722 w.FocusID(3)
723 m.ToggleFullscreen()
724 m.RemoveWindow(2)
725 m.ToggleFullscreen()
726 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 3, 4}, {}}) || len(m.Current().Columns) != 3 {
727 t.Fatal(got, m.Current().Columns)
728 }
729 })
730 t.Run("floating window", func(t *testing.T) {
731 m := fixed()
732 m.AddFloating(4, 10, 10)
733 m.ToggleFullscreen()
734 if got := windows(m); !reflect.DeepEqual(got, [][]WindowID{{1, 2, 3}, {}, {}}) || m.Active != 1 || m.Workspaces[1].floatIndex(4) != 0 {
735 t.Fatal(got, m.Active)
736 }
737 m.ToggleFullscreen()
738 if w := m.Current(); m.Active != 0 || w.floatIndex(4) != 0 || !w.floatFocus {
739 t.Fatal(m.Active, w.Floats)
740 }
741 })
742 t.Run("origin gone unlinks it", func(t *testing.T) {
743 m := fixed()
744 m.Current().FocusID(2)
745 m.ToggleFullscreen()
746 m.take(m.Workspaces[0])
747 m.ToggleFullscreen()
748 if id, _ := m.Focused(); id != 2 || m.Current().fullscreen != 0 {
749 t.Fatal(id)
750 }
751 })
752}