1package core
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12// laterClock moves fullscreenGrace forward on each Now: fullscreen
13// requests are past the grace.
14func laterClock(t *testing.T) *portsmocks.MockClock {
15 t.Helper()
16 now := time.Unix(0, 0)
17 clock := portsmocks.NewMockClock(t)
18 clock.EXPECT().Now().RunAndReturn(func() time.Time {
19 now = now.Add(fullscreenGrace)
20 return now
21 }).Maybe()
22 return clock
23}
24
25func TestArrangeLayers(t *testing.T) {
26 bar := ports.LayerSurface{ID: 1, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
27 tests := []struct {
28 name string
29 layers []ports.LayerSurface
30 rects []ports.Rect
31 usable ports.Rect
32 }{
33 {"top", []ports.LayerSurface{bar}, []ports.Rect{{W: 1920, H: 30}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
34 {"bottom margin", []ports.LayerSurface{{Anchor: ports.AnchorBottom, Width: 100, Height: 20, ExclusiveZone: 20, Margin: [4]int32{0, 0, 5, 0}}}, []ports.Rect{{X: 910, Y: 1055, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1055}},
35 {"left dock", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorTop | ports.AnchorBottom, Width: 40, Height: 1080, ExclusiveZone: 40}}, []ports.Rect{{W: 40, H: 1080}}, ports.Rect{X: 40, W: 1880, H: 1080}},
36 {"center", []ports.LayerSurface{{Width: 100, Height: 40, Layer: ports.LayerOverlay}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 40}}, ports.Rect{W: 1920, H: 1080}},
37 {"opposing horizontal anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight, Width: 100, Height: 20, Margin: [4]int32{0, 30, 0, 10}}}, []ports.Rect{{X: 900, Y: 530, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}},
38 {"top only", []ports.LayerSurface{{Anchor: ports.AnchorTop, Width: 100, Height: 20, Margin: [4]int32{7, 0, 0, 0}}}, []ports.Rect{{X: 910, Y: 7, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}},
39 {"full span between margins", []ports.LayerSurface{{Anchor: ports.AnchorLeft | ports.AnchorRight | ports.AnchorTop, Height: 20, Margin: [4]int32{7, 30, 0, 10}}}, []ports.Rect{{X: 10, Y: 7, W: 1880, H: 20}}, ports.Rect{W: 1920, H: 1080}},
40 {"opposing vertical anchors with margins", []ports.LayerSurface{{Anchor: ports.AnchorTop | ports.AnchorBottom, Width: 100, Height: 20, Margin: [4]int32{10, 0, 30, 0}}}, []ports.Rect{{X: 910, Y: 520, W: 100, H: 20}}, ports.Rect{W: 1920, H: 1080}},
41 {"minus one", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: ports.AnchorTop, Width: 100, Height: 20, ExclusiveZone: -1}}, []ports.Rect{{W: 1920, H: 30}, {X: 910, W: 100, H: 20}}, ports.Rect{Y: 30, W: 1920, H: 1050}},
42 {"stack", []ports.LayerSurface{bar, {Layer: ports.LayerTop, Anchor: bar.Anchor, Width: 1920, Height: 20, ExclusiveZone: 20}}, []ports.Rect{{W: 1920, H: 30}, {Y: 30, W: 1920, H: 20}}, ports.Rect{Y: 50, W: 1920, H: 1030}},
43 }
44 for _, tt := range tests {
45 t.Run(tt.name, func(t *testing.T) {
46 got, u := arrangeLayers(1920, 1080, tt.layers)
47 if u != tt.usable || len(got) != len(tt.rects) {
48 t.Fatalf("got %v %v", got, u)
49 }
50 for i, r := range tt.rects {
51 if got[i].Rect != r {
52 t.Fatalf("rect %d: %v want %v", i, got[i].Rect, r)
53 }
54 }
55 })
56 }
57 layers := []ports.LayerSurface{{ID: 1, Layer: ports.LayerOverlay}, {ID: 2, Layer: ports.LayerBottom}, {ID: 3, Layer: ports.LayerBackground}, {ID: 4, Layer: ports.LayerTop}, {ID: 5, Layer: ports.LayerBottom}}
58 got, _ := arrangeLayers(100, 100, layers)
59 for i, id := range []ports.WindowID{3, 2, 5, 4, 1} {
60 if got[i].ID != id {
61 t.Fatalf("order: %v", got)
62 }
63 }
64}
65
66func TestLayerChangedChannels(t *testing.T) {
67 client := make(chan ports.ClientEvent)
68 output := make(chan ports.OutputEvent)
69 commands := make(chan ports.ClientCommand, 16)
70 scenes := make(chan []ports.Scene, 1)
71 cfg := ports.Config{}
72 cfg.Keyboard.CmdKey = "super"
73 cfg.Layout.MaxColumns = 2
74 c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{})
75 if err != nil {
76 t.Fatal(err)
77 }
78 ctx, cancel := context.WithCancel(context.Background())
79 defer cancel()
80 done := make(chan struct{})
81 go func() { defer close(done); _ = c.Run(ctx) }()
82 recv := func() ports.Scene {
83 t.Helper()
84 select {
85 case s := <-scenes:
86 return s[0]
87 case <-time.After(time.Second):
88 t.Fatal("scene timeout")
89 return ports.Scene{}
90 }
91 }
92 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 1920, Height: 1080}}
93 recv()
94 client <- ports.WindowMapped{ID: 1}
95 before := recv()
96 for len(commands) > 0 {
97 <-commands
98 }
99 bar := ports.LayerSurface{ID: 2, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Width: 1920, Height: 30, ExclusiveZone: 30}
100 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
101 scene := recv()
102 if len(scene.Layers) != 1 || scene.Layers[0].Rect != (ports.Rect{W: 1920, H: 30}) || len(scene.Windows) != 1 || scene.Windows[0].Rect.Y < 30 {
103 t.Fatalf("scene: %v", scene)
104 }
105 if scene.Windows[0].Rect.H != before.Windows[0].Rect.H-30 {
106 t.Fatalf("height: %v vs %v", scene.Windows, before.Windows)
107 }
108 select {
109 case v := <-commands:
110 cfg, ok := v.(ports.ConfigureWindow)
111 if !ok || cfg.ID != 1 || cfg.Height != scene.Windows[0].Rect.H {
112 t.Fatalf("configure: %v", v)
113 }
114 case <-time.After(time.Second):
115 t.Fatal("configure timeout")
116 }
117 scene.Layers[0].Rect.Y = 99
118 client <- ports.LayerChanged{Layers: []ports.LayerSurface{bar}}
119 if next := recv(); next.Layers[0].Rect.Y != 0 {
120 t.Fatalf("scene alias: %v", next)
121 }
122 cancel()
123 <-done
124}
125
126// In scroll overflow, a fullscreen column the user scrolled off no longer
127// covers the output: the layers show again.
128func TestScrolledOffFullscreenShowsLayers(t *testing.T) {
129 m := newMonitor("", "")
130 m.SetOutput(100, 80)
131 m.AddWindow(1)
132 m.SetFullscreen(1, true)
133 m.AddWindow(2)
134 sc := &screen{mon: m}
135 bar := ports.SceneLayer{ID: 6, Layer: ports.LayerTop}
136 if shown(sc, bar) {
137 t.Fatal("bar shown over fullscreen")
138 }
139 m.Current().FocusID(2)
140 if !shown(sc, bar) {
141 t.Fatal("bar hidden with the fullscreen column scrolled off")
142 }
143}
144
145// In the overview a fullscreen window is a preview: the layers show.
146func TestOverviewOverFullscreenShowsLayers(t *testing.T) {
147 m := newMonitor("", "")
148 m.SetOutput(100, 80)
149 m.AddWindow(1)
150 m.SetFullscreen(1, true)
151 sc := &screen{mon: m}
152 bar := ports.SceneLayer{ID: 6, Layer: ports.LayerTop}
153 m.ToggleOverview()
154 if !shown(sc, bar) {
155 t.Fatal("bar hidden in the overview")
156 }
157 m.ToggleOverview()
158 if shown(sc, bar) {
159 t.Fatal("bar shown over fullscreen after the overview")
160 }
161}
162
163// A fullscreen window is exclusive: the scene carries only the background
164// and a layer taking the keyboard exclusively (a locker), so the output
165// can scan the window out.
166func TestFullscreenSceneLayers(t *testing.T) {
167 client := make(chan ports.ClientEvent)
168 output := make(chan ports.OutputEvent)
169 commands := make(chan ports.ClientCommand, 64)
170 scenes := make(chan []ports.Scene, 1)
171 cfg := ports.Config{}
172 cfg.Keyboard.CmdKey = "super"
173 cfg.Layout.MaxColumns = 2
174 c, err := New(cfg, Channels{Client: client, Output: output, Commands: commands, Scenes: scenes}, Options{Clock: laterClock(t)})
175 if err != nil {
176 t.Fatal(err)
177 }
178 ctx, cancel := context.WithCancel(context.Background())
179 defer cancel()
180 done := make(chan struct{})
181 go func() { defer close(done); _ = c.Run(ctx) }()
182 go func() {
183 for range commands {
184 }
185 }()
186 recv := func() ports.Scene {
187 t.Helper()
188 select {
189 case s := <-scenes:
190 return s[0]
191 case <-time.After(time.Second):
192 t.Fatal("scene timeout")
193 return ports.Scene{}
194 }
195 }
196 layers := func(s ports.Scene) []ports.WindowID {
197 var ids []ports.WindowID
198 for _, l := range s.Layers {
199 ids = append(ids, l.ID)
200 }
201 return ids
202 }
203 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "OUT-1", Width: 100, Height: 80}}
204 recv()
205 client <- ports.WindowMapped{ID: 1}
206 recv()
207 all := []ports.LayerSurface{
208 {ID: 5, Layer: ports.LayerBackground, Anchor: 15},
209 {ID: 6, Layer: ports.LayerTop, Anchor: ports.AnchorTop | ports.AnchorLeft | ports.AnchorRight, Height: 10},
210 {ID: 7, Layer: ports.LayerOverlay, Width: 20, Height: 10},
211 }
212 client <- ports.LayerChanged{Layers: all}
213 if got := layers(recv()); len(got) != 3 {
214 t.Fatalf("layers %v", got)
215 }
216 client <- ports.WindowFullscreenRequest{ID: 1, Fullscreen: true}
217 if got := layers(recv()); len(got) != 1 || got[0] != 5 {
218 t.Fatalf("fullscreen layers %v, want the background", got)
219 }
220 locker := ports.LayerSurface{ID: 8, Layer: ports.LayerOverlay, Anchor: 15, Keyboard: 1}
221 client <- ports.LayerChanged{Layers: append(all, locker)}
222 if got := layers(recv()); len(got) != 2 || got[1] != 8 {
223 t.Fatalf("fullscreen layers %v, want the background and the locker", got)
224 }
225 cancel()
226 <-done
227 close(commands)
228}