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// A monitor on that side wins over an occupied workspace, so the opposite
54// key brings focus back; the workspace is reached only with no monitor.
55func TestCascadeFocusColumnGoesToMonitorBeforeWorkspace(t *testing.T) {
56 c, m := axisCore(t, true, OverflowCascade, 3)
57 m.Current().FocusID(23)
58 c.applyAction(ActionFocusColumnRight)
59 wantFocus(t, c, "R", 0)
60 wantActive(t, m, 1)
61 // Back with the opposite key.
62 c.applyAction(ActionFocusColumnLeft)
63 wantFocus(t, c, "M", 23)
64 wantActive(t, m, 1)
65 // The first column of a band: the monitor on the left.
66 m.Current().FocusID(24)
67 c.applyAction(ActionFocusColumnLeft)
68 wantFocus(t, c, "L", 0)
69 wantActive(t, m, 1)
70 // From the last occupied workspace: the monitor on the right, not the
71 // trailing empty workspace.
72 c.focusScreen = 1
73 m.Focus(2)
74 m.Current().FocusID(35)
75 c.applyAction(ActionFocusColumnRight)
76 wantFocus(t, c, "R", 0)
77 wantActive(t, m, 2)
78}
79
80// With no monitor on that side, the trailing empty workspace is the last
81// stop, and left comes back from it.
82func TestCascadeFocusColumnReachesTrailingWorkspaceWithoutMonitor(t *testing.T) {
83 c, m := axisCore(t, false, OverflowCascade, 3)
84 m.Focus(2)
85 m.Current().FocusID(35)
86 c.applyAction(ActionFocusColumnRight)
87 wantActive(t, m, 3)
88 if !m.Current().empty() {
89 t.Fatal("want the trailing empty workspace")
90 }
91 c.applyAction(ActionFocusColumnLeft)
92 wantActive(t, m, 2)
93}
94
95// One monitor on the left only. Right steps to the next workspace; left
96// from its empty band leaves for that monitor, which comes first.
97func TestCascadeStepRightThenLeftLeavesForMonitor(t *testing.T) {
98 c := layoutCore(1, tile{"L", 0, 0, 900, 600}, tile{"M", 900, 0, 900, 600})
99 m := c.cur().mon
100 m.SetOverflow(OverflowCascade)
101 m.SetMaxColumns(3)
102 m.AddWindow(1)
103 m.AddWindow(5)
104 c.applyAction(ActionFocusColumnRight)
105 wantActive(t, m, 1)
106 c.applyAction(ActionFocusColumnLeft)
107 wantFocus(t, c, "L", 0)
108 wantActive(t, m, 1)
109}
110
111// AC3: up/down cross bands and stop at the last, with no workspace change.
112func TestCascadeFocusWindowDownStaysOnWorkspace(t *testing.T) {
113 c, m := axisCore(t, false, OverflowCascade, 3)
114 m.Current().FocusID(22)
115 c.applyAction(ActionFocusWindowDown)
116 wantFocus(t, c, "M", 25)
117 wantActive(t, m, 1)
118 c.applyAction(ActionFocusWindowDown)
119 wantFocus(t, c, "M", 25)
120 wantActive(t, m, 1)
121}
122
123// AC4: scroll and fixed switch workspace with up/down at the column
124// bottom; left/right at the edge do nothing without a monitor.
125func TestScrollFixedWorkspaceAxisIsVertical(t *testing.T) {
126 for _, overflow := range []Overflow{OverflowScroll, OverflowFixed} {
127 t.Run(string(overflow), func(t *testing.T) {
128 c, m := axisCore(t, false, overflow, 3)
129 m.Current().FocusID(25)
130 c.applyAction(ActionFocusColumnRight)
131 wantActive(t, m, 1)
132 wantFocus(t, c, "M", 25)
133 c.applyAction(ActionFocusColumnLeft)
134 c.applyAction(ActionFocusColumnLeft)
135 c.applyAction(ActionFocusColumnLeft)
136 c.applyAction(ActionFocusColumnLeft)
137 c.applyAction(ActionFocusColumnLeft)
138 wantActive(t, m, 1)
139 c.applyAction(ActionFocusWindowDown)
140 wantActive(t, m, 2)
141 m.Focus(1)
142 m.Current().FocusID(21) // top of the first column
143 c.applyAction(ActionFocusWindowUp)
144 wantActive(t, m, 0)
145 })
146 }
147}
148
149// AC5: a shown named workspace is outside the workspace list.
150func TestCascadeNamedWorkspaceDoesNotStep(t *testing.T) {
151 c, m := axisCore(t, false, OverflowCascade, 3)
152 m.SetNamed([]NamedWorkspace{{Name: "n"}})
153 m.ToggleNamed("n")
154 shown := m.shown
155 if shown == nil {
156 t.Fatal("named workspace not shown")
157 }
158 shown.Overflow = OverflowCascade
159 shown.SetMaxColumns(3)
160 for id := WindowID(41); id <= 45; id++ {
161 m.AddWindow(id)
162 }
163 shown.FocusID(43)
164 c.applyAction(ActionFocusColumnRight)
165 if m.shown != shown || m.Active != 1 {
166 t.Fatalf("shown %p want %p, active %d", m.shown, shown, m.Active)
167 }
168 wantFocus(t, c, "M", 43)
169 shown.FocusID(41)
170 c.applyAction(ActionFocusColumnLeft)
171 if m.shown != shown || m.Active != 1 {
172 t.Fatalf("shown %p want %p, active %d", m.shown, shown, m.Active)
173 }
174 wantFocus(t, c, "M", 41)
175}
176
177// AC6: a covering fullscreen tile at the band edge steps the workspace and
178// stays fullscreen.
179func TestCascadeFullscreenBandEdgeStepsWorkspace(t *testing.T) {
180 c, m := axisCore(t, false, OverflowCascade, 3)
181 w := m.Current()
182 w.FocusID(23)
183 w.ToggleFullscreen()
184 c.applyAction(ActionFocusColumnRight)
185 wantActive(t, m, 2)
186 if w.fullscreen != 23 {
187 t.Fatalf("fullscreen %d, want 23", w.fullscreen)
188 }
189}
190
191// A covering fullscreen tile follows the same order: the monitor on that
192// side before the occupied workspace.
193func TestCascadeFullscreenGoesToMonitorBeforeWorkspace(t *testing.T) {
194 c, m := axisCore(t, true, OverflowCascade, 3)
195 w := m.Current()
196 w.FocusID(23)
197 w.ToggleFullscreen()
198 c.applyAction(ActionFocusColumnRight)
199 wantFocus(t, c, "R", 0)
200 wantActive(t, m, 1)
201 if w.fullscreen != 23 {
202 t.Fatalf("fullscreen %d, want 23", w.fullscreen)
203 }
204}
205
206// A workspace holding only a float is occupied: with no monitor on that
207// side, it comes before the trailing empty one.
208func TestCascadeFloatOnlyWorkspaceIsOccupied(t *testing.T) {
209 c, m := axisCore(t, false, OverflowCascade, 3)
210 m.Focus(3)
211 m.AddFloating(9, 100, 100)
212 m.Focus(2)
213 m.Current().FocusID(35)
214 c.applyAction(ActionFocusColumnRight)
215 wantActive(t, m, 3)
216 if got := c.cur().name(); got != "M" {
217 t.Fatalf("focused screen %q, want M", got)
218 }
219}
220
221// AC7: a lone native float, or an empty cascade workspace, is at the edge on
222// both sides.
223func TestCascadeLoneFloatAndEmptyWorkspaceStepWorkspace(t *testing.T) {
224 c, m := axisCore(t, false, OverflowCascade, 3)
225 m.Focus(3) // the trailing empty workspace
226 if !m.Current().empty() {
227 t.Fatal("workspace 4 not empty")
228 }
229 c.applyAction(ActionFocusColumnLeft)
230 wantActive(t, m, 2)
231
232 // A lone native float on its own workspace, past the trailing one.
233 for _, tc := range []struct {
234 a Action
235 want int
236 }{{ActionFocusColumnLeft, 2}, {ActionFocusColumnRight, 4}} {
237 c, m = axisCore(t, false, OverflowCascade, 3)
238 m.Focus(3)
239 m.AddFloating(9, 100, 100)
240 if w := m.Current(); len(w.Columns) != 0 || !w.floatFocus {
241 t.Fatalf("columns %d floatFocus %v, want a lone focused float", len(w.Columns), w.floatFocus)
242 }
243 c.applyAction(tc.a)
244 wantActive(t, m, tc.want)
245 }
246}