animations_interrupt_internal_test.go RawHistoryFolder
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// movingRects makes two windows on sc swap places and returns with their
13// rect motions running, one frame in (the clock moved 16 ms and stepped).
14func movingRects(t *testing.T, c *Core, ic *indicatorClock, sc *screen) {
15 t.Helper()
16 c.cfg.Animations.On = true
17 sc.mon.AddWindow(1)
18 sc.mon.AddWindow(2)
19 settleShown(t, c)
20 c.focusScreen = c.screenIndex(sc.name())
21 before := c.snapshot(ic.now)
22 c.applyAction(ActionMoveColumnLeft)
23 c.transition(before, ic.now)
24 if len(sc.rects) != 2 {
25 t.Fatalf("%d rect motions after the move, want 2", len(sc.rects))
26 }
27 indicatorScene(t, c)
28 ic.now = ic.now.Add(16 * time.Millisecond)
29 if err := c.step(context.Background(), nil); err != nil {
30 t.Fatal(err)
31 }
32 <-c.ch.Scenes
33 if rm := sc.rects[1]; rm.dx == 0 || !rm.on() {
34 t.Fatalf("window 1 offset %v (running %t) one frame in: not mid-motion", rm.dx, rm.on())
35 }
36}
37
38// A session lock in the middle of a rect motion stops every motion and the
39// frame timer, drops the screens' last scene and shown layouts, and the
40// scene after the unlock is the settled one with a Seq nothing used before.
41func TestSessionLockMidRectMotion(t *testing.T) {
42 c, ic := indicatorCore(t)
43 state := ports.SecurityState{}
44 gate := portsmocks.NewMockSessionSecurity(t)
45 gate.EXPECT().Snapshot().RunAndReturn(func() ports.SecurityState { return state })
46 c.opts.Security = gate
47 sc := c.cur()
48 movingRects(t, c, ic, sc)
49 if !c.animating() || c.frameC == nil || sc.last.Seq == 0 {
50 t.Fatalf("nothing to interrupt: animating %t, timer %t, last seq %d", c.animating(), c.frameC != nil, sc.last.Seq)
51 }
52 seq := sc.last.Seq
53
54 state = ports.SecurityState{Generation: 1, Protected: true}
55 if err := c.publish(context.Background()); err != nil {
56 t.Fatal(err)
57 }
58 locked := (<-c.ch.Scenes)[0]
59 if len(locked.Windows) != 0 || len(locked.Separators) != 0 || locked.Seq <= seq {
60 t.Fatalf("locked scene shows the desktop or reuses a Seq (%d <= %d): %+v", locked.Seq, seq, locked)
61 }
62 if len(sc.rects) != 0 || sc.rectsWS != nil || sc.springing() || c.animating() || c.frameC != nil {
63 t.Fatalf("motion survived the lock: rects %d, springing %t, animating %t, timer %t", len(sc.rects), sc.springing(), c.animating(), c.frameC != nil)
64 }
65 if sc.last.Seq != 0 || sc.shown != nil || sc.settledLayout != nil {
66 t.Fatalf("lock kept the desktop scene: last seq %d, shown %v, settled %v", sc.last.Seq, sc.shown != nil, sc.settledLayout != nil)
67 }
68
69 // Unlocked: the windows are where the move left them, nothing runs.
70 state = ports.SecurityState{Generation: 2}
71 s := indicatorScene(t, c)
72 if s.Seq <= locked.Seq {
73 t.Fatalf("unlocked scene Seq %d reuses the locked one %d", s.Seq, locked.Seq)
74 }
75 var got1, got2 Rect
76 for _, w := range s.Windows {
77 switch w.ID {
78 case 1:
79 got1 = w.Rect
80 case 2:
81 got2 = w.Rect
82 }
83 }
84 // 300x200 output, two columns of 150: the move put 2 first.
85 if got2.X != 0 || got1.X != 150 {
86 t.Fatalf("after the unlock 1 at %+v and 2 at %+v, want the settled 150 and 0", got1, got2)
87 }
88 if c.animating() || len(sc.rects) != 0 || c.frameC != nil {
89 t.Fatalf("a motion runs after the unlock: animating %t, rects %d, timer %t", c.animating(), len(sc.rects), c.frameC != nil)
90 }
91}
92
93// An output unplugged in the middle of its windows' rect motion: no panic,
94// its motions go with it, a late page flip for it does nothing, and the
95// remaining output keeps no motion of the gone one.
96func TestOutputUnplugMidRectMotion(t *testing.T) {
97 c, ic := indicatorCore(t)
98 a := c.cur()
99 c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200})
100 b := c.screens[1]
101 movingRects(t, c, ic, b)
102 c.removeScreen("B")
103 if c.screenIndex("B") >= 0 || len(c.screens) != 1 {
104 t.Fatalf("B still plugged: %d screens", len(c.screens))
105 }
106 if len(a.rects) != 0 || a.springing() {
107 t.Fatalf("A inherited the motions of B: rects %d, springing %t", len(a.rects), a.springing())
108 }
109 if c.sliding("B") {
110 t.Fatal("the gone output still slides")
111 }
112 // B's windows are guests of A now: the next publish and a stray flip are fine.
113 if err := c.publish(context.Background()); err != nil {
114 t.Fatal(err)
115 }
116 <-c.ch.Scenes
117 ic.now = ic.now.Add(5 * time.Second)
118 if err := c.step(context.Background(), nil); err != nil {
119 t.Fatal(err)
120 }
121 <-c.ch.Scenes
122 if c.animating() || c.frameC != nil {
123 t.Fatalf("still animating after the unplug and a settle: animating %t, timer %t", c.animating(), c.frameC != nil)
124 }
125}
126
127// The last output going away keeps its screen as a placeholder: none of its
128// springs (rect motions, a workspace switch) may outlive the output, or the
129// fallback timer would wake core for a screen nothing draws. When an output
130// comes back, a new move animates and lands from a fresh Seq.
131func TestLastOutputUnplugMidRectMotion(t *testing.T) {
132 c, ic := indicatorCore(t)
133 sc := c.cur()
134 movingRects(t, c, ic, sc)
135 // A workspace switch spring runs too.
136 m := sc.mon
137 m.switchView.off, m.switchView.motion = 0.5, c.spring(workspaceSpring(0.5, 0), ic.now)
138 if !m.switchView.motion.on || !sc.springing() {
139 t.Fatal("no workspace switch to interrupt")
140 }
141 c.removeScreen("A")
142 if sc.mon.springing() || len(sc.rects) != 0 || sc.rectsWS != nil || sc.mon.switchView.motion.on || sc.mon.switchView.off != 0 {
143 t.Fatalf("the placeholder keeps springs: monitor %t, rects %d, switch %t (%v)", sc.mon.springing(), len(sc.rects), sc.mon.switchView.motion.on, sc.mon.switchView.off)
144 }
145 if err := c.publish(context.Background()); err != nil {
146 t.Fatal(err)
147 }
148 gone := (<-c.ch.Scenes)[0]
149 if c.animating() || c.frameC != nil {
150 t.Fatalf("the placeholder keeps asking for frames: animating %t, timer %t", c.animating(), c.frameC != nil)
151 }
152
153 // The output comes back: its windows are where the move left them.
154 c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200})
155 settled := indicatorScene(t, c)
156 if settled.Seq <= gone.Seq {
157 t.Fatalf("scene after the replug reuses Seq %d (placeholder %d)", settled.Seq, gone.Seq)
158 }
159 x := func(s ports.Scene, id WindowID) int {
160 for _, w := range s.Windows {
161 if w.ID == id {
162 return w.Rect.X
163 }
164 }
165 t.Fatalf("window %d not in the scene", id)
166 return 0
167 }
168 if x(settled, 2) != 0 || x(settled, 1) != 150 {
169 t.Fatalf("after the replug 1 at %d and 2 at %d, want the settled 150 and 0", x(settled, 1), x(settled, 2))
170 }
171 if c.animating() || len(c.cur().rects) != 0 {
172 t.Fatal("a motion runs after the replug")
173 }
174
175 // A new move animates from the settled rects and lands.
176 before := c.snapshot(ic.now)
177 c.applyAction(ActionMoveColumnRight)
178 c.transition(before, ic.now)
179 start := indicatorScene(t, c)
180 if x(start, 1) != 150 || x(start, 2) != 0 {
181 t.Fatalf("right after the move 1 at %d and 2 at %d, want the old 150 and 0", x(start, 1), x(start, 2))
182 }
183 ic.now = ic.now.Add(16 * time.Millisecond)
184 if err := c.step(context.Background(), nil); err != nil {
185 t.Fatal(err)
186 }
187 mid := (<-c.ch.Scenes)[0]
188 if got := x(mid, 1); got >= 150 || got <= 0 {
189 t.Fatalf("window 1 at %d on the first frame, want between 0 and 150", got)
190 }
191 if mid.Seq == start.Seq {
192 t.Fatalf("an animating frame reuses Seq %d", mid.Seq)
193 }
194 ic.now = ic.now.Add(5 * time.Second)
195 if err := c.step(context.Background(), nil); err != nil {
196 t.Fatal(err)
197 }
198 end := (<-c.ch.Scenes)[0]
199 if x(end, 1) != 0 || x(end, 2) != 150 || end.Seq == mid.Seq {
200 t.Fatalf("landed 1 at %d and 2 at %d (Seq %d after %d), want 0 and 150 and a fresh Seq", x(end, 1), x(end, 2), end.Seq, mid.Seq)
201 }
202 if c.animating() || c.frameC != nil {
203 t.Fatal("still animating after the landing")
204 }
205}