1package core
2
3import "testing"
4
5// axisCore is a cascade core whose monitor "M" has three workspaces of five
6// windows (ids 11-15, 21-25, 31-35; bands of three) and a trailing empty
7// one. Workspace 2 is active. With sides, monitors L and R flank M.
8func axisCore(t *testing.T, sides bool, overflow Overflow, maxCols int) (*Core, *Monitor) {
9 t.Helper()
10 c, _ := cascadeMovesCore(t, 0, maxCols, sides)
11 m := c.cur().mon
12 m.SetOverflow(overflow)
13 m.SetMaxColumns(maxCols)
14 for ws := range 3 {
15 m.Focus(ws)
16 for i := 1; i <= 5; i++ {
17 m.AddWindow(WindowID((ws+1)*10 + i))
18 }
19 }
20 m.Focus(1)
21 return c, m
22}
23
24func wantActive(t *testing.T, m *Monitor, active int) {
25 t.Helper()
26 if m.Active != active {
27 t.Fatalf("active workspace %d, want %d", m.Active, active)
28 }
29}
30
31// AC1: past the band edge with no monitor, left/right step the workspace.
32func TestCascadeFocusColumnStepsWorkspaceAtBandEdge(t *testing.T) {
33 c, m := axisCore(t, false, OverflowCascade, 3)
34 m.Current().FocusID(23) // last column of band 1, not the last band
35 c.applyAction(ActionFocusColumnRight)
36 wantActive(t, m, 2)
37 m.Focus(1)
38 m.Current().FocusID(24) // first column of band 2
39 c.applyAction(ActionFocusColumnLeft)
40 wantActive(t, m, 0)
41 // The first workspace stays put at the start.
42 m.Current().FocusID(11)
43 c.applyAction(ActionFocusColumnLeft)
44 wantActive(t, m, 0)
45 // Inside the band, focus moves and the workspace stays.
46 c.applyAction(ActionFocusColumnRight)
47 wantActive(t, m, 0)
48 if id, _ := m.Current().Focused(); id != 12 {
49 t.Fatalf("focused %d, want 12", id)
50 }
51}
52
53// An occupied workspace wins over a monitor on that side, so the opposite
54// key brings focus back; the monitor wins over the trailing empty
55// workspace, so it stays reachable.
56func TestCascadeFocusColumnStepsWorkspaceBeforeMonitor(t *testing.T) {
57 c, m := axisCore(t, true, OverflowCascade, 3)
58 m.Current().FocusID(23)
59 c.applyAction(ActionFocusColumnRight)
60 wantActive(t, m, 2)
61 if got := c.cur().name(); got != "M" {
62 t.Fatalf("focused screen %q, want M", got)
63 }
64 // Back with the opposite key, from the first column of workspace 3.
65 m.Current().FocusID(31)
66 c.applyAction(ActionFocusColumnLeft)
67 wantActive(t, m, 1)
68 if got := c.cur().name(); got != "M" {
69 t.Fatalf("focused screen %q, want M", got)
70 }
71 // Past the first workspace: the monitor on the left.
72 m.Focus(0)
73 m.Current().FocusID(11)
74 c.applyAction(ActionFocusColumnLeft)
75 wantFocus(t, c, "L", 0)
76 wantActive(t, m, 0)
77 // From the last occupied workspace: the monitor on the right, not the
78 // trailing empty workspace.
79 c.focusScreen = 1
80 m.Focus(2)
81 m.Current().FocusID(35)
82 c.applyAction(ActionFocusColumnRight)
83 wantFocus(t, c, "R", 0)
84 wantActive(t, m, 2)
85}
86
87// With no monitor on that side, the trailing empty workspace is the last
88// stop, and left comes back from it.
89func TestCascadeFocusColumnReachesTrailingWorkspaceWithoutMonitor(t *testing.T) {
90 c, m := axisCore(t, false, OverflowCascade, 3)
91 m.Focus(2)
92 m.Current().FocusID(35)
93 c.applyAction(ActionFocusColumnRight)
94 wantActive(t, m, 3)
95 if !m.Current().empty() {
96 t.Fatal("want the trailing empty workspace")
97 }
98 c.applyAction(ActionFocusColumnLeft)
99 wantActive(t, m, 2)
100}
101
102// The live regression: one monitor on the left only. Right steps to the
103// next workspace, and left from its empty band comes back to the previous
104// workspace instead of leaving for that monitor.
105func TestCascadeStepRightThenLeftComesBack(t *testing.T) {
106 c := layoutCore(1, tile{"L", 0, 0, 900, 600}, tile{"M", 900, 0, 900, 600})
107 m := c.cur().mon
108 m.SetOverflow(OverflowCascade)
109 m.SetMaxColumns(3)
110 m.AddWindow(1)
111 m.AddWindow(5)
112 c.applyAction(ActionFocusColumnRight)
113 wantActive(t, m, 1)
114 c.applyAction(ActionFocusColumnLeft)
115 wantFocus(t, c, "M", 5)
116 wantActive(t, m, 0)
117}
118
119// AC3: up/down cross bands and stop at the last, with no workspace change.
120func TestCascadeFocusWindowDownStaysOnWorkspace(t *testing.T) {
121 c, m := axisCore(t, false, OverflowCascade, 3)
122 m.Current().FocusID(22)
123 c.applyAction(ActionFocusWindowDown)
124 wantFocus(t, c, "M", 25)
125 wantActive(t, m, 1)
126 c.applyAction(ActionFocusWindowDown)
127 wantFocus(t, c, "M", 25)
128 wantActive(t, m, 1)
129}
130
131// AC4: scroll and fixed switch workspace with up/down at the column
132// bottom; left/right at the edge do nothing without a monitor.
133func TestScrollFixedWorkspaceAxisIsVertical(t *testing.T) {
134 for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
135 t.Run(string(overflow), func(t *testing.T) {
136 c, m := axisCore(t, false, overflow, 3)
137 m.Current().FocusID(25)
138 c.applyAction(ActionFocusColumnRight)
139 wantActive(t, m, 1)
140 wantFocus(t, c, "M", 25)
141 c.applyAction(ActionFocusColumnLeft)
142 c.applyAction(ActionFocusColumnLeft)
143 c.applyAction(ActionFocusColumnLeft)
144 c.applyAction(ActionFocusColumnLeft)
145 c.applyAction(ActionFocusColumnLeft)
146 wantActive(t, m, 1)
147 c.applyAction(ActionFocusWindowDown)
148 wantActive(t, m, 2)
149 m.Focus(1)
150 m.Current().FocusID(21) // top of the first column
151 c.applyAction(ActionFocusWindowUp)
152 wantActive(t, m, 0)
153 })
154 }
155}
156
157// AC5: a shown named workspace is outside the workspace list.
158func TestCascadeNamedWorkspaceDoesNotStep(t *testing.T) {
159 c, m := axisCore(t, false, OverflowCascade, 3)
160 m.SetNamed([]NamedWorkspace{{Name: "n"}})
161 m.ToggleNamed("n")
162 shown := m.shown
163 if shown == nil {
164 t.Fatal("named workspace not shown")
165 }
166 shown.Overflow = OverflowCascade
167 shown.SetMaxColumns(3)
168 for id := WindowID(41); id <= 45; id++ {
169 m.AddWindow(id)
170 }
171 shown.FocusID(43)
172 c.applyAction(ActionFocusColumnRight)
173 if m.shown != shown || m.Active != 1 {
174 t.Fatalf("shown %p want %p, active %d", m.shown, shown, m.Active)
175 }
176 wantFocus(t, c, "M", 43)
177 shown.FocusID(41)
178 c.applyAction(ActionFocusColumnLeft)
179 if m.shown != shown || m.Active != 1 {
180 t.Fatalf("shown %p want %p, active %d", m.shown, shown, m.Active)
181 }
182 wantFocus(t, c, "M", 41)
183}
184
185// AC6: a covering fullscreen tile at the band edge steps the workspace and
186// stays fullscreen.
187func TestCascadeFullscreenBandEdgeStepsWorkspace(t *testing.T) {
188 c, m := axisCore(t, false, OverflowCascade, 3)
189 w := m.Current()
190 w.FocusID(23)
191 w.ToggleFullscreen()
192 c.applyAction(ActionFocusColumnRight)
193 wantActive(t, m, 2)
194 if w.fullscreen != 23 {
195 t.Fatalf("fullscreen %d, want 23", w.fullscreen)
196 }
197}
198
199// A covering fullscreen tile follows the same order: the occupied workspace
200// before the monitor on that side, which comes back only past the last one.
201func TestCascadeFullscreenStepsWorkspaceBeforeMonitor(t *testing.T) {
202 c, m := axisCore(t, true, OverflowCascade, 3)
203 w := m.Current()
204 w.FocusID(23)
205 w.ToggleFullscreen()
206 c.applyAction(ActionFocusColumnRight)
207 wantActive(t, m, 2)
208 if got := c.cur().name(); got != "M" {
209 t.Fatalf("focused screen %q, want M", got)
210 }
211 if w.fullscreen != 23 {
212 t.Fatalf("fullscreen %d, want 23", w.fullscreen)
213 }
214 last := m.Current()
215 last.FocusID(35)
216 last.ToggleFullscreen()
217 c.applyAction(ActionFocusColumnRight)
218 wantFocus(t, c, "R", 0)
219 if last.fullscreen != 35 {
220 t.Fatalf("fullscreen %d, want 35", last.fullscreen)
221 }
222}
223
224// A workspace holding only a float is occupied: it comes before the monitor.
225func TestCascadeFloatOnlyWorkspaceIsOccupied(t *testing.T) {
226 c, m := axisCore(t, true, OverflowCascade, 3)
227 m.Focus(3)
228 m.AddFloating(9, 100, 100)
229 m.Focus(2)
230 m.Current().FocusID(35)
231 c.applyAction(ActionFocusColumnRight)
232 wantActive(t, m, 3)
233 if got := c.cur().name(); got != "M" {
234 t.Fatalf("focused screen %q, want M", got)
235 }
236}
237
238// AC7: a lone native float, or an empty cascade workspace, is at the edge on
239// both sides.
240func TestCascadeLoneFloatAndEmptyWorkspaceStepWorkspace(t *testing.T) {
241 c, m := axisCore(t, false, OverflowCascade, 3)
242 m.Focus(3) // the trailing empty workspace
243 if !m.Current().empty() {
244 t.Fatal("workspace 4 not empty")
245 }
246 c.applyAction(ActionFocusColumnLeft)
247 wantActive(t, m, 2)
248
249 // A lone native float on its own workspace, past the trailing one.
250 for _, tc := range []struct {
251 a Action
252 want int
253 }{{ActionFocusColumnLeft, 2}, {ActionFocusColumnRight, 4}} {
254 c, m = axisCore(t, false, OverflowCascade, 3)
255 m.Focus(3)
256 m.AddFloating(9, 100, 100)
257 if w := m.Current(); len(w.Columns) != 0 || !w.floatFocus {
258 t.Fatalf("columns %d floatFocus %v, want a lone focused float", len(w.Columns), w.floatFocus)
259 }
260 c.applyAction(tc.a)
261 wantActive(t, m, tc.want)
262 }
263}