1package core
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9)
10
11// hiddenCapture is a 300x200 output whose first workspace (windows 1 and 2,
12// a 100x100 viewport) is off screen, captured.
13func hiddenCapture(t *testing.T) (*Core, *Workspace) {
14 t.Helper()
15 c, ws, _ := hiddenCaptureCommands(t)
16 return c, ws
17}
18
19// hiddenCaptureCommands is hiddenCapture with the channel core sends its commands on.
20func hiddenCaptureCommands(t *testing.T) (*Core, *Workspace, chan ports.ClientCommand) {
21 t.Helper()
22 commands := make(chan ports.ClientCommand, 64)
23 var cfg ports.Config
24 cfg.Keyboard.CmdKey = "super"
25 cfg.Layout.MaxColumns = 2
26 cfg.Border.Width = 2
27 cfg.Background.Color = "#102030"
28 c, err := New(cfg, Channels{
29 Scenes: make(chan []ports.Scene, 1), Commands: commands,
30 })
31 if err != nil {
32 t.Fatal(err)
33 }
34 c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200})
35 m := c.screens[0].mon
36 ws := m.Current()
37 ws.SetSize(100, 100)
38 m.AddWindow(1)
39 m.AddWindow(2)
40 m.Focus(1)
41 if m.Current() == ws {
42 t.Fatal("workspace still on screen")
43 }
44 c.configures.cw.load(c.screens[0], ws)
45 return c, ws, commands
46}
47
48func TestCaptureConfigureSizesHiddenWindowWithoutShowingIt(t *testing.T) {
49 c, _ := hiddenCapture(t)
50 sc := c.screens[0]
51 phys := Placement{ID: 1, Hidden: true}
52 t0 := configureTarget{output: "A", area: sc.mon.Frame()}
53 cp, tt := c.captureConfigure(sc, phys, t0)
54 if cp == nil || cp.Hidden {
55 t.Fatalf("no real placement: %+v", cp)
56 }
57 v, send := c.configures.nextWithCapture(phys, tt, cp)
58 want := c.clientRect(*cp)
59 if !send || v.Width != want.W || v.Height != want.H || want.W == 0 {
60 t.Fatalf("configure %+v send %v want size %+v", v, send, want)
61 }
62 if v.Visible || v.Activated || !v.Captured || v.Output != "A" {
63 t.Fatalf("captured hidden window must be sized, unfocused, physically invisible: %+v", v)
64 }
65 c.configures.mark(v)
66 if _, send := c.configures.nextWithCapture(phys, tt, cp); send {
67 t.Fatal("unchanged captured configure sent again")
68 }
69 // The session stops: the window is only hidden again, keeps its size and drops Captured.
70 c.configures.cw.reset()
71 cp, tt = c.captureConfigure(sc, phys, t0)
72 if cp != nil {
73 t.Fatal("placement kept after the session")
74 }
75 v2, send := c.configures.nextWithCapture(phys, tt, cp)
76 if !send || v2.Captured || v2.Visible || v2.Width != v.Width || v2.Height != v.Height {
77 t.Fatalf("released window %+v send %v after %+v", v2, send, v)
78 }
79}
80
81func TestCaptureConfigureLeavesOtherWindowsAlone(t *testing.T) {
82 c, _ := hiddenCapture(t)
83 sc := c.screens[0]
84 phys := Placement{ID: 99, Hidden: true}
85 t0 := configureTarget{output: "A", area: sc.mon.Frame()}
86 if cp, tt := c.captureConfigure(sc, phys, t0); cp != nil || tt != t0 {
87 t.Fatalf("window of another workspace captured: %+v", cp)
88 }
89 v, _ := c.configures.nextWithCapture(phys, t0, nil)
90 if v.Captured || v.Visible || v.Width != 0 {
91 t.Fatalf("plain hidden configure %+v", v)
92 }
93}
94
95func TestCaptureSceneRebasesOnViewport(t *testing.T) {
96 c, ws := hiddenCapture(t)
97 f := ws.Output
98 if f != (Rect{X: 100, Y: 50, W: 100, H: 100}) {
99 t.Fatalf("viewport %+v", f)
100 }
101 c.popups[10] = &popupState{id: 10, parent: 1, rect: Rect{X: 3, Y: 4, W: 10, H: 8}, mapped: true}
102 c.popupOrder = append(c.popupOrder, 10)
103 s := c.captureScene(7)
104 if s == nil || s.OutputWidth != 100 || s.OutputHeight != 100 || s.WorkspaceClip != (Rect{W: 100, H: 100}) || s.Seq != 7 || s.Output != "A" {
105 t.Fatalf("scene %+v", s)
106 }
107 if s.Capture != nil || s.CaptureScene != nil || len(s.Layers) != 0 || s.Background != "#102030" || s.Border.Width != 2 {
108 t.Fatalf("capture scene must be flat, layerless and use the normal config: %+v", s)
109 }
110 byID := map[WindowID]ports.SceneWindow{}
111 for _, w := range s.Windows {
112 byID[w.ID] = w
113 }
114 for _, p := range ws.Layout() {
115 w := byID[p.ID]
116 if want := (Rect{X: p.Rect.X - f.X, Y: p.Rect.Y - f.Y, W: p.Rect.W, H: p.Rect.H}); w.Rect != want || w.Focused {
117 t.Fatalf("window %d rect %+v want %+v focused %v", p.ID, w.Rect, want, w.Focused)
118 }
119 if w.Rect.X < 0 || w.Rect.Y < 0 || w.Rect.X+w.Rect.W > 100 || w.Rect.Y+w.Rect.H > 100 {
120 t.Fatalf("window %d outside its viewport: %+v", p.ID, w.Rect)
121 }
122 }
123 client := c.clientRect(ws.Layout()[0])
124 pop := byID[10]
125 if !pop.Popup || pop.Rect != (Rect{X: client.X - f.X + 3, Y: client.Y - f.Y + 4, W: 10, H: 8}) {
126 t.Fatalf("popup %+v client %+v", pop, client)
127 }
128 for _, sep := range s.Separators {
129 if sep.Rect.X < 0 || sep.Rect.Y < 0 || sep.Rect.X+sep.Rect.W > 100 || sep.Rect.Y+sep.Rect.H > 100 {
130 t.Fatalf("separator outside the viewport: %+v", sep)
131 }
132 }
133 c.configures.cw.reset()
134 if c.captureScene(8) != nil {
135 t.Fatal("scene without a captured workspace")
136 }
137}
138
139func TestCapturedPopupsSurviveButHiddenWindowsTakeNoInput(t *testing.T) {
140 c, _ := hiddenCapture(t)
141 c.popups[10] = &popupState{id: 10, parent: 1, rect: Rect{X: 3, Y: 4, W: 10, H: 8}, mapped: true}
142 c.popupOrder = append(c.popupOrder, 10)
143 if _, _, ok := c.windowRect(10); ok {
144 t.Fatal("captured popup is physically on screen")
145 }
146 if _, _, ok := c.windowRect(1); ok || c.visible(1) {
147 t.Fatal("captured window is physically on screen")
148 }
149 if id, _, _ := c.hit(150, 100); id != 0 {
150 t.Fatalf("pointer hit hidden window %d", id)
151 }
152 if sc := c.screens[0]; len(c.scenePopups(sc)) != 0 {
153 t.Fatal("captured popup drawn on the physical scene")
154 }
155 if err := c.closeHiddenPopups(context.Background()); err != nil {
156 t.Fatal(err)
157 }
158 if c.popups[10] == nil {
159 t.Fatal("popup of a captured window dismissed")
160 }
161 c.configures.cw.reset()
162 if err := c.closeHiddenPopups(context.Background()); err != nil {
163 t.Fatal(err)
164 }
165 if c.popups[10] != nil {
166 t.Fatal("popup of a hidden window kept after the session")
167 }
168}
169
170func TestCaptureWorkspaceReusesStorage(t *testing.T) {
171 c, ws := hiddenCapture(t)
172 sc := c.screens[0]
173 cw := &c.configures.cw
174 layoutAllocs := testing.AllocsPerRun(50, func() { _ = ws.Layout() })
175 loadAllocs := testing.AllocsPerRun(50, func() { cw.load(sc, ws) })
176 if loadAllocs > layoutAllocs {
177 t.Fatalf("load allocates %v, more than the layout it wraps (%v)", loadAllocs, layoutAllocs)
178 }
179 if got := testing.AllocsPerRun(50, func() {
180 cw.reset()
181 cw.load(sc, ws)
182 }); got > layoutAllocs {
183 t.Fatalf("reset+load allocates %v, layout %v", got, layoutAllocs)
184 }
185}
186
187// While the captured workspace itself slides, the live (Active) session keeps
188// tracking it in its own viewport: its windows stay captured and its popups
189// stay, and both are forgotten when the session ends.
190func TestCapturedWorkspaceStaysCapturedWhileSliding(t *testing.T) {
191 c, ws, _ := hiddenCaptureCommands(t)
192 c.configures.cw.reset()
193 c.capt.sessions = []*capSession{{open: ports.CaptureSessionOpen{ID: 1, Workspace: ws.ID}}}
194 c.popups[10] = &popupState{id: 10, parent: 1, rect: Rect{X: 3, Y: 4, W: 10, H: 8}, mapped: true}
195 c.popupOrder = append(c.popupOrder, 10)
196 ws.motion = newMotion(viewSpring(10, 0), time.Time{})
197 ctx := context.Background()
198 if err := c.publish(ctx); err != nil {
199 t.Fatal(err)
200 }
201 if v := c.capt.sessions[0].last; !v.Hidden || !v.Active || v.Reason.Terminal() {
202 t.Fatalf("session state %+v", v)
203 }
204 if !c.configures.cw.active() || c.popups[10] == nil {
205 t.Fatal("sliding session lost its captured workspace or popup")
206 }
207 if v, ok := c.configures.sent[1]; !ok || !v.Captured || v.Visible || v.Activated || v.Width <= 0 {
208 t.Fatalf("window during the slide %+v (sent %v)", v, ok)
209 }
210 ws.motion = nil
211 if err := c.publish(ctx); err != nil {
212 t.Fatal(err)
213 }
214 if !c.configures.cw.active() {
215 t.Fatalf("captured workspace lost after the slide: %+v", c.capt.sessions[0].last)
216 }
217 // The session ends: the captured workspace is forgotten and its popup goes.
218 c.captureClose(1)
219 if err := c.publish(ctx); err != nil {
220 t.Fatal(err)
221 }
222 if c.configures.cw.active() || c.popups[10] != nil {
223 t.Fatal("captured workspace or popup kept after the session")
224 }
225 if v, ok := c.configures.sent[1]; !ok || v.Captured || v.Visible || v.Width <= 0 {
226 t.Fatalf("window after the session %+v (sent %v)", v, ok)
227 }
228}
229
230// In the overview the captured workspace's windows keep their real size and
231// their physical state; nothing is sized as a thumbnail.
232func TestCapturedWorkspaceInOverviewKeepsRealSize(t *testing.T) {
233 c, ws, _ := hiddenCaptureCommands(t)
234 c.configures.cw.reset()
235 m := c.screens[0].mon
236 m.Focus(0)
237 if m.Current() != ws {
238 t.Fatal("workspace not current")
239 }
240 ctx := context.Background()
241 if err := c.publish(ctx); err != nil {
242 t.Fatal(err)
243 }
244 before := map[WindowID]ports.ConfigureWindow{}
245 for _, p := range ws.Layout() {
246 v, ok := c.configures.sent[p.ID]
247 if !ok || v.Width <= 0 || !v.Visible || v.Captured {
248 t.Fatalf("window %d before the overview %+v (sent %v)", p.ID, v, ok)
249 }
250 before[p.ID] = v
251 }
252 m.ToggleOverview()
253 c.capt.sessions = []*capSession{{open: ports.CaptureSessionOpen{ID: 1, Workspace: ws.ID}}}
254 if err := c.publish(ctx); err != nil {
255 t.Fatal(err)
256 }
257 if !c.configures.cw.active() {
258 t.Fatal("overview did not track the captured workspace")
259 }
260 for _, p := range ws.Layout() {
261 v, ok := c.configures.sent[p.ID]
262 if !ok {
263 t.Fatalf("window %d has no configure in the overview", p.ID)
264 }
265 // A preview keeps the client's real size and its physical state; it
266 // is on screen, so it is not Captured.
267 if v.Width != before[p.ID].Width || v.Height != before[p.ID].Height || v.Width != c.clientRect(p).W || v.Height != c.clientRect(p).H {
268 t.Fatalf("window %d configured %dx%d in the overview, real %dx%d", p.ID, v.Width, v.Height, c.clientRect(p).W, c.clientRect(p).H)
269 }
270 if !v.Visible || v.Captured || v.Activated != before[p.ID].Activated {
271 t.Fatalf("window %d shown as a preview but %+v, was %+v", p.ID, v, before[p.ID])
272 }
273 }
274}