test(core): cover fresh Seq, fallback timer, chained velocity and transitions
Adds tests for the animating Seq, the fallback timer under another output's flips, velocity carry-over, click and discrete-swipe transitions, a following swipe and framed switches. Folds the pulse transition test into the slide table, merges the moved-workspace tests, strengthens the overview test, and removes two duplicates.
- commit
- 4cbd8f89908f95bec1ff8c90fbbcab0190e9b552
- author
- brice · · 2026-10-04 19:06 UTC
- committer
- brice · · 2026-10-04 19:08 UTC
- parent
- 3f1045d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
8 files · +398 -109
M internal/core/animations_off_test.go +7 -2
| @@ -66,11 +66,16 @@ func TestReloadToOffSettlesALanding(t *testing.T) { | |||
| 66 | 66 | ||
| 67 | 67 | // noFrameScene sends page flips and expects no scene: no spring runs, so | |
| 68 | 68 | // the flips have nothing to move and publish nothing. | |
| 69 | - | func (r *swipeRig) noFrameScene(t *testing.T) { | |
| 69 | + | func (r *swipeRig) noFrameScene(t *testing.T, outputs ...string) { | |
| 70 | 70 | t.Helper() | |
| 71 | + | if len(outputs) == 0 { | |
| 72 | + | outputs = []string{wide.Name} | |
| 73 | + | } | |
| 71 | 74 | for range 2 { | |
| 72 | 75 | r.advance(16 * time.Millisecond) | |
| 73 | - | r.frames <- ports.OutputFrame{Output: wide.Name} | |
| 76 | + | for _, o := range outputs { | |
| 77 | + | r.frames <- ports.OutputFrame{Output: o} | |
| 78 | + | } | |
| 74 | 79 | } | |
| 75 | 80 | select { | |
| 76 | 81 | case s := <-r.scenes: | |
| @@ -66,11 +66,16 @@ func TestReloadToOffSettlesALanding(t *testing.T) { | |||||
| 66 | 66 | ||||
| 67 | // noFrameScene sends page flips and expects no scene: no spring runs, so | 67 | // noFrameScene sends page flips and expects no scene: no spring runs, so | ||
| 68 | // the flips have nothing to move and publish nothing. | 68 | // the flips have nothing to move and publish nothing. | ||
| 69 | - | func (r *swipeRig) noFrameScene(t *testing.T) { | 69 | + | func (r *swipeRig) noFrameScene(t *testing.T, outputs ...string) { |
| 70 | t.Helper() | 70 | t.Helper() | ||
| 71 | + | if len(outputs) == 0 { | |||
| 72 | + | outputs = []string{wide.Name} | |||
| 73 | + | } | |||
| 71 | for range 2 { | 74 | for range 2 { | ||
| 72 | r.advance(16 * time.Millisecond) | 75 | r.advance(16 * time.Millisecond) | ||
| 73 | - | r.frames <- ports.OutputFrame{Output: wide.Name} | 76 | + | for _, o := range outputs { |
| 77 | + | r.frames <- ports.OutputFrame{Output: o} | |||
| 78 | + | } | |||
| 74 | } | 79 | } | ||
| 75 | select { | 80 | select { | ||
| 76 | case s := <-r.scenes: | 81 | case s := <-r.scenes: | ||
M internal/core/animations_stop_test.go +1 -15
| @@ -1,9 +1,6 @@ | |||
| 1 | 1 | package core | |
| 2 | 2 | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | "time" | |
| 6 | - | ) | |
| 3 | + | import "testing" | |
| 7 | 4 | ||
| 8 | 5 | func TestStopAnimationsSettlesEverySpring(t *testing.T) { | |
| 9 | 6 | c, ic := indicatorCore(t) | |
| @@ -22,14 +19,3 @@ func TestStopAnimationsSettlesEverySpring(t *testing.T) { | |||
| 22 | 19 | t.Fatalf("springs left: shift %v, switch %v, motion %v %v, timer %v", w.shift, m.switchOff, w.motion.on, m.switchMotion.on, c.frameC != nil) | |
| 23 | 20 | } | |
| 24 | 21 | } | |
| 25 | - | ||
| 26 | - | func TestSpringAppliesSlowdown(t *testing.T) { | |
| 27 | - | c, ic := indicatorCore(t) | |
| 28 | - | c.cfg.Animations.Slowdown = 1 | |
| 29 | - | one := c.spring(viewSpring(40, 0), ic.now) | |
| 30 | - | c.cfg.Animations.Slowdown = 2 | |
| 31 | - | two := c.spring(viewSpring(40, 0), ic.now) | |
| 32 | - | if d := two.end - 2*one.end; d < -time.Microsecond || d > time.Microsecond { | |
| 33 | - | t.Fatalf("slowdown 2 ends at %v, slowdown 1 at %v", two.end, one.end) | |
| 34 | - | } | |
| 35 | - | } | |
| @@ -1,9 +1,6 @@ | |||||
| 1 | package core | 1 | package core | ||
| 2 | 2 | ||||
| 3 | - | import ( | 3 | + | import "testing" |
| 4 | - | "testing" | |||
| 5 | - | "time" | |||
| 6 | - | ) | |||
| 7 | 4 | ||||
| 8 | func TestStopAnimationsSettlesEverySpring(t *testing.T) { | 5 | func TestStopAnimationsSettlesEverySpring(t *testing.T) { | ||
| 9 | c, ic := indicatorCore(t) | 6 | c, ic := indicatorCore(t) | ||
| @@ -22,14 +19,3 @@ func TestStopAnimationsSettlesEverySpring(t *testing.T) { | |||||
| 22 | t.Fatalf("springs left: shift %v, switch %v, motion %v %v, timer %v", w.shift, m.switchOff, w.motion.on, m.switchMotion.on, c.frameC != nil) | 19 | t.Fatalf("springs left: shift %v, switch %v, motion %v %v, timer %v", w.shift, m.switchOff, w.motion.on, m.switchMotion.on, c.frameC != nil) | ||
| 23 | } | 20 | } | ||
| 24 | } | 21 | } | ||
| 25 | - | ||||
| 26 | - | func TestSpringAppliesSlowdown(t *testing.T) { | |||
| 27 | - | c, ic := indicatorCore(t) | |||
| 28 | - | c.cfg.Animations.Slowdown = 1 | |||
| 29 | - | one := c.spring(viewSpring(40, 0), ic.now) | |||
| 30 | - | c.cfg.Animations.Slowdown = 2 | |||
| 31 | - | two := c.spring(viewSpring(40, 0), ic.now) | |||
| 32 | - | if d := two.end - 2*one.end; d < -time.Microsecond || d > time.Microsecond { | |||
| 33 | - | t.Fatalf("slowdown 2 ends at %v, slowdown 1 at %v", two.end, one.end) | |||
| 34 | - | } | |||
| 35 | - | } | |||
M internal/core/pulse_test.go +31 -7
| @@ -185,30 +185,54 @@ func TestFocusPulseSkipsAloneOnScreen(t *testing.T) { | |||
| 185 | 185 | } | |
| 186 | 186 | } | |
| 187 | 187 | ||
| 188 | - | // A swipe or its landing slide still shows the neighbours: the pulse waits | |
| 189 | - | // for the layout to settle, then decides on it. | |
| 188 | + | // A swipe, its landing slide or a transition still shows the neighbours: the | |
| 189 | + | // pulse waits for the layout to settle, then decides on it. | |
| 190 | 190 | func TestFocusPulseWaitsForSlide(t *testing.T) { | |
| 191 | 191 | for _, tc := range []struct { | |
| 192 | 192 | name string | |
| 193 | - | slide func(*Monitor, time.Time) | |
| 193 | + | slide func(*testing.T, *Core, time.Time) | |
| 194 | 194 | alone bool | |
| 195 | 195 | }{ | |
| 196 | - | {"workspace landing, alone", func(m *Monitor, now time.Time) { m.switchMotion = newMotion(workspaceSpring(0.5, 0), now, 1) }, true}, | |
| 197 | - | {"workspace drag, alone", func(m *Monitor, _ time.Time) { m.switchOff = 0.3 }, true}, | |
| 198 | - | {"column drag, beside another", func(m *Monitor, _ time.Time) { m.Current().shift = 40 }, false}, | |
| 196 | + | {"workspace landing, alone", func(_ *testing.T, c *Core, now time.Time) { | |
| 197 | + | c.cur().mon.switchMotion = newMotion(workspaceSpring(0.5, 0), now, 1) | |
| 198 | + | }, true}, | |
| 199 | + | {"workspace drag, alone", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.switchOff = 0.3 }, true}, | |
| 200 | + | {"column drag, beside another", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.Current().shift = 40 }, false}, | |
| 201 | + | {"rect motion, beside another", func(_ *testing.T, c *Core, now time.Time) { | |
| 202 | + | sc := c.cur() | |
| 203 | + | sc.rects = map[WindowID]rectMotion{1: {x: c.spring(viewSpring(40, 0), now), dx: 40}} | |
| 204 | + | sc.rectsWS = sc.mon.Current() | |
| 205 | + | }, false}, | |
| 206 | + | {"camera transition of a key, beside another", func(t *testing.T, c *Core, now time.Time) { | |
| 207 | + | m := c.cur().mon | |
| 208 | + | for id := WindowID(3); id <= 4; id++ { | |
| 209 | + | m.AddWindow(id) | |
| 210 | + | } | |
| 211 | + | indicatorScene(t, c) | |
| 212 | + | before := c.snapshot() | |
| 213 | + | for range 3 { | |
| 214 | + | c.applyAction(ActionFocusColumnLeft) | |
| 215 | + | } | |
| 216 | + | c.transition(before, now) | |
| 217 | + | if !m.Current().motion.on || m.settled() { | |
| 218 | + | t.Fatal("no camera transition after the focus moved the view") | |
| 219 | + | } | |
| 220 | + | }, false}, | |
| 199 | 221 | } { | |
| 200 | 222 | t.Run(tc.name, func(t *testing.T) { | |
| 201 | 223 | c, ic := pulseCore(t) | |
| 202 | 224 | m := c.cur().mon | |
| 203 | 225 | m.Current().FocusID(1) | |
| 204 | 226 | indicatorScene(t, c) | |
| 205 | - | tc.slide(m, ic.now) | |
| 227 | + | tc.slide(t, c, ic.now) | |
| 228 | + | indicatorScene(t, c) | |
| 206 | 229 | ic.now = ic.now.Add(pulseSettle) | |
| 207 | 230 | if c.pulseTick() || c.pulse.timerC == nil { | |
| 208 | 231 | t.Fatal("pulse decided during a slide") | |
| 209 | 232 | } | |
| 210 | 233 | m.stopSwitch() | |
| 211 | 234 | m.Current().stopSlide() | |
| 235 | + | c.cur().stopRects() | |
| 212 | 236 | if tc.alone { | |
| 213 | 237 | m.Current().ToggleFullWidth() | |
| 214 | 238 | } | |
| @@ -185,30 +185,54 @@ func TestFocusPulseSkipsAloneOnScreen(t *testing.T) { | |||||
| 185 | } | 185 | } | ||
| 186 | } | 186 | } | ||
| 187 | 187 | ||||
| 188 | - | // A swipe or its landing slide still shows the neighbours: the pulse waits | 188 | + | // A swipe, its landing slide or a transition still shows the neighbours: the |
| 189 | - | // for the layout to settle, then decides on it. | 189 | + | // pulse waits for the layout to settle, then decides on it. |
| 190 | func TestFocusPulseWaitsForSlide(t *testing.T) { | 190 | func TestFocusPulseWaitsForSlide(t *testing.T) { | ||
| 191 | for _, tc := range []struct { | 191 | for _, tc := range []struct { | ||
| 192 | name string | 192 | name string | ||
| 193 | - | slide func(*Monitor, time.Time) | 193 | + | slide func(*testing.T, *Core, time.Time) |
| 194 | alone bool | 194 | alone bool | ||
| 195 | }{ | 195 | }{ | ||
| 196 | - | {"workspace landing, alone", func(m *Monitor, now time.Time) { m.switchMotion = newMotion(workspaceSpring(0.5, 0), now, 1) }, true}, | 196 | + | {"workspace landing, alone", func(_ *testing.T, c *Core, now time.Time) { |
| 197 | - | {"workspace drag, alone", func(m *Monitor, _ time.Time) { m.switchOff = 0.3 }, true}, | 197 | + | c.cur().mon.switchMotion = newMotion(workspaceSpring(0.5, 0), now, 1) |
| 198 | - | {"column drag, beside another", func(m *Monitor, _ time.Time) { m.Current().shift = 40 }, false}, | 198 | + | }, true}, |
| 199 | + | {"workspace drag, alone", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.switchOff = 0.3 }, true}, | |||
| 200 | + | {"column drag, beside another", func(_ *testing.T, c *Core, _ time.Time) { c.cur().mon.Current().shift = 40 }, false}, | |||
| 201 | + | {"rect motion, beside another", func(_ *testing.T, c *Core, now time.Time) { | |||
| 202 | + | sc := c.cur() | |||
| 203 | + | sc.rects = map[WindowID]rectMotion{1: {x: c.spring(viewSpring(40, 0), now), dx: 40}} | |||
| 204 | + | sc.rectsWS = sc.mon.Current() | |||
| 205 | + | }, false}, | |||
| 206 | + | {"camera transition of a key, beside another", func(t *testing.T, c *Core, now time.Time) { | |||
| 207 | + | m := c.cur().mon | |||
| 208 | + | for id := WindowID(3); id <= 4; id++ { | |||
| 209 | + | m.AddWindow(id) | |||
| 210 | + | } | |||
| 211 | + | indicatorScene(t, c) | |||
| 212 | + | before := c.snapshot() | |||
| 213 | + | for range 3 { | |||
| 214 | + | c.applyAction(ActionFocusColumnLeft) | |||
| 215 | + | } | |||
| 216 | + | c.transition(before, now) | |||
| 217 | + | if !m.Current().motion.on || m.settled() { | |||
| 218 | + | t.Fatal("no camera transition after the focus moved the view") | |||
| 219 | + | } | |||
| 220 | + | }, false}, | |||
| 199 | } { | 221 | } { | ||
| 200 | t.Run(tc.name, func(t *testing.T) { | 222 | t.Run(tc.name, func(t *testing.T) { | ||
| 201 | c, ic := pulseCore(t) | 223 | c, ic := pulseCore(t) | ||
| 202 | m := c.cur().mon | 224 | m := c.cur().mon | ||
| 203 | m.Current().FocusID(1) | 225 | m.Current().FocusID(1) | ||
| 204 | indicatorScene(t, c) | 226 | indicatorScene(t, c) | ||
| 205 | - | tc.slide(m, ic.now) | 227 | + | tc.slide(t, c, ic.now) |
| 228 | + | indicatorScene(t, c) | |||
| 206 | ic.now = ic.now.Add(pulseSettle) | 229 | ic.now = ic.now.Add(pulseSettle) | ||
| 207 | if c.pulseTick() || c.pulse.timerC == nil { | 230 | if c.pulseTick() || c.pulse.timerC == nil { | ||
| 208 | t.Fatal("pulse decided during a slide") | 231 | t.Fatal("pulse decided during a slide") | ||
| 209 | } | 232 | } | ||
| 210 | m.stopSwitch() | 233 | m.stopSwitch() | ||
| 211 | m.Current().stopSlide() | 234 | m.Current().stopSlide() | ||
| 235 | + | c.cur().stopRects() | |||
| 212 | if tc.alone { | 236 | if tc.alone { | ||
| 213 | m.Current().ToggleFullWidth() | 237 | m.Current().ToggleFullWidth() | ||
| 214 | } | 238 | } | ||
M internal/core/scene_seq_test.go +52 -0
| @@ -24,3 +24,55 @@ func TestSceneSeqKeptWhenNothingChanged(t *testing.T) { | |||
| 24 | 24 | t.Fatalf("Seq %d after mapping a window (was %d), %d windows", next.Seq, first.Seq, len(next.Windows)) | |
| 25 | 25 | } | |
| 26 | 26 | } | |
| 27 | + | ||
| 28 | + | // A key that starts a transition can publish a scene that draws what the | |
| 29 | + | // previous one did (the springs start there): it still gets a fresh Seq, or | |
| 30 | + | // the outputs would drop it and the motion would wait for the fallback timer | |
| 31 | + | // to begin. | |
| 32 | + | func TestAnimatingKeyGetsFreshSeq(t *testing.T) { | |
| 33 | + | tests := []struct { | |
| 34 | + | name string | |
| 35 | + | edit func(*ports.Config) | |
| 36 | + | // setup returns the scene before the key. | |
| 37 | + | setup func(*testing.T, *swipeRig) ports.Scene | |
| 38 | + | sym string | |
| 39 | + | mods ports.Mods | |
| 40 | + | // same: the key's scene draws what the previous one did. | |
| 41 | + | same bool | |
| 42 | + | }{ | |
| 43 | + | {"width change", func(c *ports.Config) { c.Binds["Cmd+Ctrl+Right"] = "set-column-width +10%" }, | |
| 44 | + | func(t *testing.T, r *swipeRig) ports.Scene { | |
| 45 | + | twoColumns(t, r) | |
| 46 | + | return r.key(t, "Left", ports.ModAlt)[0] | |
| 47 | + | }, "Right", ports.ModAlt | ports.ModCtrl, true}, | |
| 48 | + | {"focus scroll", nil, func(t *testing.T, r *swipeRig) ports.Scene { | |
| 49 | + | threeColumns(t, r) | |
| 50 | + | return r.key(t, "Left", ports.ModAlt)[0] // 2 is on screen: no scroll | |
| 51 | + | }, "Left", ports.ModAlt, false}, | |
| 52 | + | } | |
| 53 | + | for _, tt := range tests { | |
| 54 | + | t.Run(tt.name, func(t *testing.T) { | |
| 55 | + | r := startSwipe(t, tt.edit) | |
| 56 | + | prev := tt.setup(t, r) | |
| 57 | + | s := r.key(t, tt.sym, tt.mods)[0] | |
| 58 | + | if s.SameAs(prev) != tt.same { | |
| 59 | + | t.Fatalf("scene right after the key same as the previous one: %t, want %t", s.SameAs(prev), tt.same) | |
| 60 | + | } | |
| 61 | + | if s.Seq <= prev.Seq { | |
| 62 | + | t.Fatalf("Seq %d right after the key, want a new one (was %d)", s.Seq, prev.Seq) | |
| 63 | + | } | |
| 64 | + | }) | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // A key that animates nothing keeps its output's Seq. | |
| 69 | + | func TestNoOpKeyKeepsSeqWithAnimationsOn(t *testing.T) { | |
| 70 | + | r := startSwipe(t, nil) | |
| 71 | + | threeColumns(t, r) | |
| 72 | + | first := r.key(t, "Right", ports.ModAlt)[0] // 3 is the last column | |
| 73 | + | for range 2 { | |
| 74 | + | if s := r.key(t, "Right", ports.ModAlt)[0]; s.Seq != first.Seq || !s.SameAs(first) { | |
| 75 | + | t.Fatalf("Seq %d on a key at the edge, want the held %d", s.Seq, first.Seq) | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | } | |
| @@ -24,3 +24,55 @@ func TestSceneSeqKeptWhenNothingChanged(t *testing.T) { | |||||
| 24 | t.Fatalf("Seq %d after mapping a window (was %d), %d windows", next.Seq, first.Seq, len(next.Windows)) | 24 | t.Fatalf("Seq %d after mapping a window (was %d), %d windows", next.Seq, first.Seq, len(next.Windows)) | ||
| 25 | } | 25 | } | ||
| 26 | } | 26 | } | ||
| 27 | + | ||||
| 28 | + | // A key that starts a transition can publish a scene that draws what the | |||
| 29 | + | // previous one did (the springs start there): it still gets a fresh Seq, or | |||
| 30 | + | // the outputs would drop it and the motion would wait for the fallback timer | |||
| 31 | + | // to begin. | |||
| 32 | + | func TestAnimatingKeyGetsFreshSeq(t *testing.T) { | |||
| 33 | + | tests := []struct { | |||
| 34 | + | name string | |||
| 35 | + | edit func(*ports.Config) | |||
| 36 | + | // setup returns the scene before the key. | |||
| 37 | + | setup func(*testing.T, *swipeRig) ports.Scene | |||
| 38 | + | sym string | |||
| 39 | + | mods ports.Mods | |||
| 40 | + | // same: the key's scene draws what the previous one did. | |||
| 41 | + | same bool | |||
| 42 | + | }{ | |||
| 43 | + | {"width change", func(c *ports.Config) { c.Binds["Cmd+Ctrl+Right"] = "set-column-width +10%" }, | |||
| 44 | + | func(t *testing.T, r *swipeRig) ports.Scene { | |||
| 45 | + | twoColumns(t, r) | |||
| 46 | + | return r.key(t, "Left", ports.ModAlt)[0] | |||
| 47 | + | }, "Right", ports.ModAlt | ports.ModCtrl, true}, | |||
| 48 | + | {"focus scroll", nil, func(t *testing.T, r *swipeRig) ports.Scene { | |||
| 49 | + | threeColumns(t, r) | |||
| 50 | + | return r.key(t, "Left", ports.ModAlt)[0] // 2 is on screen: no scroll | |||
| 51 | + | }, "Left", ports.ModAlt, false}, | |||
| 52 | + | } | |||
| 53 | + | for _, tt := range tests { | |||
| 54 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 55 | + | r := startSwipe(t, tt.edit) | |||
| 56 | + | prev := tt.setup(t, r) | |||
| 57 | + | s := r.key(t, tt.sym, tt.mods)[0] | |||
| 58 | + | if s.SameAs(prev) != tt.same { | |||
| 59 | + | t.Fatalf("scene right after the key same as the previous one: %t, want %t", s.SameAs(prev), tt.same) | |||
| 60 | + | } | |||
| 61 | + | if s.Seq <= prev.Seq { | |||
| 62 | + | t.Fatalf("Seq %d right after the key, want a new one (was %d)", s.Seq, prev.Seq) | |||
| 63 | + | } | |||
| 64 | + | }) | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // A key that animates nothing keeps its output's Seq. | |||
| 69 | + | func TestNoOpKeyKeepsSeqWithAnimationsOn(t *testing.T) { | |||
| 70 | + | r := startSwipe(t, nil) | |||
| 71 | + | threeColumns(t, r) | |||
| 72 | + | first := r.key(t, "Right", ports.ModAlt)[0] // 3 is the last column | |||
| 73 | + | for range 2 { | |||
| 74 | + | if s := r.key(t, "Right", ports.ModAlt)[0]; s.Seq != first.Seq || !s.SameAs(first) { | |||
| 75 | + | t.Fatalf("Seq %d on a key at the edge, want the held %d", s.Seq, first.Seq) | |||
| 76 | + | } | |||
| 77 | + | } | |||
| 78 | + | } | |||
M internal/core/swipe_test.go +6 -12
| @@ -477,7 +477,7 @@ func TestSwipeIgnoredAfterSwitchingAwayAndBack(t *testing.T) { | |||
| 477 | 477 | } | |
| 478 | 478 | ||
| 479 | 479 | func TestSwipeKeptWhenAnotherOutputSwitchesWorkspace(t *testing.T) { | |
| 480 | - | r := startSwipe(t, animationsOff) | |
| 480 | + | r := startSwipe(t, nil) | |
| 481 | 481 | before, _ := rectOf(threeColumns(t, r), 2) | |
| 482 | 482 | r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}) | |
| 483 | 483 | // Window 4 on DP-2, so it has a workspace below to switch to. | |
| @@ -492,7 +492,11 @@ func TestSwipeKeptWhenAnotherOutputSwitchesWorkspace(t *testing.T) { | |||
| 492 | 492 | // The pointer goes to DP-2 and a bind switches its workspace. | |
| 493 | 493 | r.input <- ports.PointerMotion{X: 1000, Y: 300} | |
| 494 | 494 | scene(t, r.scenes) | |
| 495 | - | for _, sc := range r.key(t, "Next", ports.ModAlt) { | |
| 495 | + | r.key(t, "Next", ports.ModAlt) | |
| 496 | + | // DP-2 slides to its other workspace on its own flips. | |
| 497 | + | r.advance(5 * time.Second) | |
| 498 | + | r.frames <- ports.OutputFrame{Output: "DP-2"} | |
| 499 | + | for _, sc := range receive(t, r.scenes) { | |
| 496 | 500 | if _, ok := rectOf(sc, 4); ok { | |
| 497 | 501 | t.Fatal("DP-2 did not switch workspace") | |
| 498 | 502 | } | |
| @@ -567,16 +571,6 @@ func (r *swipeRig) drain() { | |||
| 567 | 571 | } | |
| 568 | 572 | } | |
| 569 | 573 | ||
| 570 | - | func TestKeyWithoutSlideMovesAtOnce(t *testing.T) { | |
| 571 | - | r := startSwipe(t, animationsOff) | |
| 572 | - | threeColumns(t, r) | |
| 573 | - | r.key(t, "Left", ports.ModAlt) | |
| 574 | - | s := r.key(t, "Left", ports.ModAlt)[0] | |
| 575 | - | if got, _ := rectOf(s, 1); got.X != 0 { | |
| 576 | - | t.Fatalf("column 1 at %d", got.X) | |
| 577 | - | } | |
| 578 | - | } | |
| 579 | - | ||
| 580 | 574 | // In the overview a three-finger swipe does not slide the view: nothing | |
| 581 | 575 | // is drawn while the fingers move, and when they lift the selection moves | |
| 582 | 576 | // one column, the previews where they were. | |
| @@ -477,7 +477,7 @@ func TestSwipeIgnoredAfterSwitchingAwayAndBack(t *testing.T) { | |||||
| 477 | } | 477 | } | ||
| 478 | 478 | ||||
| 479 | func TestSwipeKeptWhenAnotherOutputSwitchesWorkspace(t *testing.T) { | 479 | func TestSwipeKeptWhenAnotherOutputSwitchesWorkspace(t *testing.T) { | ||
| 480 | - | r := startSwipe(t, animationsOff) | 480 | + | r := startSwipe(t, nil) |
| 481 | before, _ := rectOf(threeColumns(t, r), 2) | 481 | before, _ := rectOf(threeColumns(t, r), 2) | ||
| 482 | r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}) | 482 | r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}) | ||
| 483 | // Window 4 on DP-2, so it has a workspace below to switch to. | 483 | // Window 4 on DP-2, so it has a workspace below to switch to. | ||
| @@ -492,7 +492,11 @@ func TestSwipeKeptWhenAnotherOutputSwitchesWorkspace(t *testing.T) { | |||||
| 492 | // The pointer goes to DP-2 and a bind switches its workspace. | 492 | // The pointer goes to DP-2 and a bind switches its workspace. | ||
| 493 | r.input <- ports.PointerMotion{X: 1000, Y: 300} | 493 | r.input <- ports.PointerMotion{X: 1000, Y: 300} | ||
| 494 | scene(t, r.scenes) | 494 | scene(t, r.scenes) | ||
| 495 | - | for _, sc := range r.key(t, "Next", ports.ModAlt) { | 495 | + | r.key(t, "Next", ports.ModAlt) |
| 496 | + | // DP-2 slides to its other workspace on its own flips. | |||
| 497 | + | r.advance(5 * time.Second) | |||
| 498 | + | r.frames <- ports.OutputFrame{Output: "DP-2"} | |||
| 499 | + | for _, sc := range receive(t, r.scenes) { | |||
| 496 | if _, ok := rectOf(sc, 4); ok { | 500 | if _, ok := rectOf(sc, 4); ok { | ||
| 497 | t.Fatal("DP-2 did not switch workspace") | 501 | t.Fatal("DP-2 did not switch workspace") | ||
| 498 | } | 502 | } | ||
| @@ -567,16 +571,6 @@ func (r *swipeRig) drain() { | |||||
| 567 | } | 571 | } | ||
| 568 | } | 572 | } | ||
| 569 | 573 | ||||
| 570 | - | func TestKeyWithoutSlideMovesAtOnce(t *testing.T) { | |||
| 571 | - | r := startSwipe(t, animationsOff) | |||
| 572 | - | threeColumns(t, r) | |||
| 573 | - | r.key(t, "Left", ports.ModAlt) | |||
| 574 | - | s := r.key(t, "Left", ports.ModAlt)[0] | |||
| 575 | - | if got, _ := rectOf(s, 1); got.X != 0 { | |||
| 576 | - | t.Fatalf("column 1 at %d", got.X) | |||
| 577 | - | } | |||
| 578 | - | } | |||
| 579 | - | ||||
| 580 | // In the overview a three-finger swipe does not slide the view: nothing | 574 | // In the overview a three-finger swipe does not slide the view: nothing | ||
| 581 | // is drawn while the fingers move, and when they lift the selection moves | 575 | // is drawn while the fingers move, and when they lift the selection moves | ||
| 582 | // one column, the previews where they were. | 576 | // one column, the previews where they were. | ||
A internal/core/transition_chain_internal_test.go +208 -0
| @@ -0,0 +1,208 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // settleShown publishes so the screens' shown layouts are current. | |
| 12 | + | func settleShown(t *testing.T, c *Core) { | |
| 13 | + | t.Helper() | |
| 14 | + | indicatorScene(t, c) | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | // A bind that stops a spring while it acts (Monitor.Focus) must not lose its | |
| 18 | + | // speed: the transition that follows starts from what the snapshot recorded. | |
| 19 | + | func TestChainedActionsKeepVelocity(t *testing.T) { | |
| 20 | + | tests := []struct { | |
| 21 | + | name string | |
| 22 | + | setup func(*Core) | |
| 23 | + | first []Action | |
| 24 | + | second []Action | |
| 25 | + | // velocity is the speed of the motion the first action started. | |
| 26 | + | velocity func(*Core) float64 | |
| 27 | + | }{ | |
| 28 | + | {"camera", func(c *Core) { | |
| 29 | + | m := c.cur().mon | |
| 30 | + | for id := WindowID(1); id <= 4; id++ { | |
| 31 | + | m.AddWindow(id) | |
| 32 | + | } | |
| 33 | + | }, []Action{ActionFocusColumnLeft, ActionFocusColumnLeft, ActionFocusColumnLeft}, | |
| 34 | + | []Action{ActionFocusColumnRight, ActionFocusColumnRight, ActionFocusColumnRight}, | |
| 35 | + | func(c *Core) float64 { return c.cur().mon.Current().motion.velocity() }}, | |
| 36 | + | {"workspace switch", func(c *Core) { | |
| 37 | + | m := c.cur().mon | |
| 38 | + | m.AddWindow(1) | |
| 39 | + | m.Focus(1) | |
| 40 | + | m.AddWindow(2) | |
| 41 | + | m.Focus(0) | |
| 42 | + | }, []Action{ActionFocusWorkspaceDown}, []Action{ActionFocusWorkspaceDown}, | |
| 43 | + | func(c *Core) float64 { return c.cur().mon.switchMotion.velocity() }}, | |
| 44 | + | {"rect", func(c *Core) { | |
| 45 | + | m := c.cur().mon | |
| 46 | + | m.AddWindow(1) | |
| 47 | + | m.AddWindow(2) | |
| 48 | + | }, []Action{ActionMoveColumnLeft}, []Action{ActionMoveColumnRight}, | |
| 49 | + | func(c *Core) float64 { return c.cur().rects[1].x.velocity() }}, | |
| 50 | + | } | |
| 51 | + | for _, tt := range tests { | |
| 52 | + | t.Run(tt.name, func(t *testing.T) { | |
| 53 | + | c, ic := indicatorCore(t) | |
| 54 | + | c.cfg.Animations.On = true | |
| 55 | + | tt.setup(c) | |
| 56 | + | settleShown(t, c) | |
| 57 | + | act := func(actions []Action) { | |
| 58 | + | before := c.snapshot() | |
| 59 | + | for _, a := range actions { | |
| 60 | + | c.applyAction(a) | |
| 61 | + | } | |
| 62 | + | c.transition(before, ic.now) | |
| 63 | + | settleShown(t, c) | |
| 64 | + | } | |
| 65 | + | act(tt.first) | |
| 66 | + | if !c.animating() { | |
| 67 | + | t.Fatal("the first action started no motion") | |
| 68 | + | } | |
| 69 | + | ic.now = ic.now.Add(30 * time.Millisecond) | |
| 70 | + | c.animate(ic.now, nil) | |
| 71 | + | settleShown(t, c) | |
| 72 | + | want := tt.velocity(c) | |
| 73 | + | if want == 0 { | |
| 74 | + | t.Fatal("no speed to carry over") | |
| 75 | + | } | |
| 76 | + | act(tt.second) | |
| 77 | + | if got := tt.velocity(c); got != want { | |
| 78 | + | t.Fatalf("velocity %v after the second action, want the carried %v", got, want) | |
| 79 | + | } | |
| 80 | + | }) | |
| 81 | + | } | |
| 82 | + | } | |
| 83 | + | ||
| 84 | + | // The fallback timer guards every animating output: a page flip of one | |
| 85 | + | // output (step with only set) keeps it while another output still animates. | |
| 86 | + | func TestStepKeepsFallbackTimerWhileAnotherOutputAnimates(t *testing.T) { | |
| 87 | + | c, ic := indicatorCore(t) | |
| 88 | + | c.cfg.Animations.On = true | |
| 89 | + | a := c.cur() | |
| 90 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |
| 91 | + | b := c.screens[1] | |
| 92 | + | a.mon.AddWindow(1) | |
| 93 | + | b.mon.AddWindow(2) | |
| 94 | + | settleShown(t, c) | |
| 95 | + | for _, sc := range []*screen{a, b} { | |
| 96 | + | w := sc.mon.Current() | |
| 97 | + | w.shift, w.motion = 100, c.spring(viewSpring(100, 0), ic.now) | |
| 98 | + | } | |
| 99 | + | c.armFrame() | |
| 100 | + | armed := c.frameC | |
| 101 | + | ic.now = ic.now.Add(16 * time.Millisecond) | |
| 102 | + | if err := c.step(context.Background(), a); err != nil { | |
| 103 | + | t.Fatal(err) | |
| 104 | + | } | |
| 105 | + | <-c.ch.Scenes | |
| 106 | + | if c.frameC == nil || c.frameC != armed { | |
| 107 | + | t.Fatal("a flip of A re-armed or dropped the fallback timer while B animates") | |
| 108 | + | } | |
| 109 | + | ||
| 110 | + | // With nothing else animating the flip's own publish decides. | |
| 111 | + | b.mon.Current().stopSlide() | |
| 112 | + | if err := c.step(context.Background(), a); err != nil { | |
| 113 | + | t.Fatal(err) | |
| 114 | + | } | |
| 115 | + | <-c.ch.Scenes | |
| 116 | + | if c.frameC == nil || c.frameC == armed { | |
| 117 | + | t.Fatal("A still animates but its timer was not re-armed") | |
| 118 | + | } | |
| 119 | + | ic.now = ic.now.Add(5 * time.Second) | |
| 120 | + | if err := c.step(context.Background(), nil); err != nil { | |
| 121 | + | t.Fatal(err) | |
| 122 | + | } | |
| 123 | + | <-c.ch.Scenes | |
| 124 | + | if c.animating() || c.frameC != nil { | |
| 125 | + | t.Fatalf("settled but the timer runs (%v)", c.frameC != nil) | |
| 126 | + | } | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | // A window that left the layout while its rect motion ran drops the motion: | |
| 130 | + | // it would ask for frames with nothing to move. | |
| 131 | + | func TestRefreshShownDropsRectsOfWindowsOutOfLayout(t *testing.T) { | |
| 132 | + | c, ic := indicatorCore(t) | |
| 133 | + | c.cfg.Animations.On = true | |
| 134 | + | sc := c.cur() | |
| 135 | + | sc.mon.AddWindow(1) | |
| 136 | + | sc.mon.AddWindow(2) | |
| 137 | + | settleShown(t, c) | |
| 138 | + | sp := viewSpring(40, 0) | |
| 139 | + | sc.rects = map[WindowID]rectMotion{ | |
| 140 | + | 1: {x: c.spring(sp, ic.now), dx: 40}, | |
| 141 | + | 99: {x: c.spring(sp, ic.now), dx: 40}, | |
| 142 | + | } | |
| 143 | + | sc.rectsWS = sc.mon.Current() | |
| 144 | + | settleShown(t, c) | |
| 145 | + | if _, ok := sc.rects[99]; ok { | |
| 146 | + | t.Fatal("a window outside the layout keeps its rect motion") | |
| 147 | + | } | |
| 148 | + | if _, ok := sc.rects[1]; !ok { | |
| 149 | + | t.Fatal("a window in the layout lost its rect motion") | |
| 150 | + | } | |
| 151 | + | } | |
| 152 | + | ||
| 153 | + | // A swipe that follows the fingers owns the view: a focus bind pressed | |
| 154 | + | // meanwhile starts no camera motion. | |
| 155 | + | func TestNoCameraTransitionWhileSwipeFollows(t *testing.T) { | |
| 156 | + | c, ic := indicatorCore(t) | |
| 157 | + | c.cfg.Animations.On = true | |
| 158 | + | sc := c.cur() | |
| 159 | + | for id := WindowID(1); id <= 4; id++ { | |
| 160 | + | sc.mon.AddWindow(id) | |
| 161 | + | } | |
| 162 | + | settleShown(t, c) | |
| 163 | + | w := sc.mon.Current() | |
| 164 | + | w.shift = 25 | |
| 165 | + | c.swipe = &swipeGesture{screen: sc, ws: w, mode: swipeColumns} | |
| 166 | + | before := c.snapshot() | |
| 167 | + | viewX := w.ViewX | |
| 168 | + | for range 3 { | |
| 169 | + | c.applyAction(ActionFocusColumnLeft) | |
| 170 | + | } | |
| 171 | + | if w.ViewX == viewX { | |
| 172 | + | t.Fatal("the focus did not move the view: the case checks nothing") | |
| 173 | + | } | |
| 174 | + | c.transition(before, ic.now) | |
| 175 | + | if w.motion.on || w.shift != 25 { | |
| 176 | + | t.Fatalf("camera motion %v, shift %v: the transition took over the fingers' view", w.motion.on, w.shift) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | // A switch to or from a workspace with its own size stays settled: no | |
| 181 | + | // monitor-wide slide would be cropped to it. The same switch between | |
| 182 | + | // workspaces of the monitor's size slides. | |
| 183 | + | func TestNoSwitchMotionForSizedWorkspace(t *testing.T) { | |
| 184 | + | for _, tc := range []struct { | |
| 185 | + | name string | |
| 186 | + | sized bool | |
| 187 | + | }{{"sized", true}, {"unsized", false}} { | |
| 188 | + | t.Run(tc.name, func(t *testing.T) { | |
| 189 | + | c, ic := indicatorCore(t) | |
| 190 | + | c.cfg.Animations.On = true | |
| 191 | + | m := c.cur().mon | |
| 192 | + | m.AddWindow(1) | |
| 193 | + | if tc.sized { | |
| 194 | + | m.Workspaces[1].SetSize(100, 100) | |
| 195 | + | } | |
| 196 | + | settleShown(t, c) | |
| 197 | + | before := c.snapshot() | |
| 198 | + | m.Focus(1) | |
| 199 | + | c.transition(before, ic.now) | |
| 200 | + | if got := m.switchMotion.on; got == tc.sized { | |
| 201 | + | t.Fatalf("switch motion %v with a sized workspace %v", got, tc.sized) | |
| 202 | + | } | |
| 203 | + | if tc.sized && m.switchOff != 0 { | |
| 204 | + | t.Fatalf("switchOff %v left for a framed switch", m.switchOff) | |
| 205 | + | } | |
| 206 | + | }) | |
| 207 | + | } | |
| 208 | + | } | |
| @@ -0,0 +1,208 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // settleShown publishes so the screens' shown layouts are current. | |||
| 12 | + | func settleShown(t *testing.T, c *Core) { | |||
| 13 | + | t.Helper() | |||
| 14 | + | indicatorScene(t, c) | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | // A bind that stops a spring while it acts (Monitor.Focus) must not lose its | |||
| 18 | + | // speed: the transition that follows starts from what the snapshot recorded. | |||
| 19 | + | func TestChainedActionsKeepVelocity(t *testing.T) { | |||
| 20 | + | tests := []struct { | |||
| 21 | + | name string | |||
| 22 | + | setup func(*Core) | |||
| 23 | + | first []Action | |||
| 24 | + | second []Action | |||
| 25 | + | // velocity is the speed of the motion the first action started. | |||
| 26 | + | velocity func(*Core) float64 | |||
| 27 | + | }{ | |||
| 28 | + | {"camera", func(c *Core) { | |||
| 29 | + | m := c.cur().mon | |||
| 30 | + | for id := WindowID(1); id <= 4; id++ { | |||
| 31 | + | m.AddWindow(id) | |||
| 32 | + | } | |||
| 33 | + | }, []Action{ActionFocusColumnLeft, ActionFocusColumnLeft, ActionFocusColumnLeft}, | |||
| 34 | + | []Action{ActionFocusColumnRight, ActionFocusColumnRight, ActionFocusColumnRight}, | |||
| 35 | + | func(c *Core) float64 { return c.cur().mon.Current().motion.velocity() }}, | |||
| 36 | + | {"workspace switch", func(c *Core) { | |||
| 37 | + | m := c.cur().mon | |||
| 38 | + | m.AddWindow(1) | |||
| 39 | + | m.Focus(1) | |||
| 40 | + | m.AddWindow(2) | |||
| 41 | + | m.Focus(0) | |||
| 42 | + | }, []Action{ActionFocusWorkspaceDown}, []Action{ActionFocusWorkspaceDown}, | |||
| 43 | + | func(c *Core) float64 { return c.cur().mon.switchMotion.velocity() }}, | |||
| 44 | + | {"rect", func(c *Core) { | |||
| 45 | + | m := c.cur().mon | |||
| 46 | + | m.AddWindow(1) | |||
| 47 | + | m.AddWindow(2) | |||
| 48 | + | }, []Action{ActionMoveColumnLeft}, []Action{ActionMoveColumnRight}, | |||
| 49 | + | func(c *Core) float64 { return c.cur().rects[1].x.velocity() }}, | |||
| 50 | + | } | |||
| 51 | + | for _, tt := range tests { | |||
| 52 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 53 | + | c, ic := indicatorCore(t) | |||
| 54 | + | c.cfg.Animations.On = true | |||
| 55 | + | tt.setup(c) | |||
| 56 | + | settleShown(t, c) | |||
| 57 | + | act := func(actions []Action) { | |||
| 58 | + | before := c.snapshot() | |||
| 59 | + | for _, a := range actions { | |||
| 60 | + | c.applyAction(a) | |||
| 61 | + | } | |||
| 62 | + | c.transition(before, ic.now) | |||
| 63 | + | settleShown(t, c) | |||
| 64 | + | } | |||
| 65 | + | act(tt.first) | |||
| 66 | + | if !c.animating() { | |||
| 67 | + | t.Fatal("the first action started no motion") | |||
| 68 | + | } | |||
| 69 | + | ic.now = ic.now.Add(30 * time.Millisecond) | |||
| 70 | + | c.animate(ic.now, nil) | |||
| 71 | + | settleShown(t, c) | |||
| 72 | + | want := tt.velocity(c) | |||
| 73 | + | if want == 0 { | |||
| 74 | + | t.Fatal("no speed to carry over") | |||
| 75 | + | } | |||
| 76 | + | act(tt.second) | |||
| 77 | + | if got := tt.velocity(c); got != want { | |||
| 78 | + | t.Fatalf("velocity %v after the second action, want the carried %v", got, want) | |||
| 79 | + | } | |||
| 80 | + | }) | |||
| 81 | + | } | |||
| 82 | + | } | |||
| 83 | + | ||||
| 84 | + | // The fallback timer guards every animating output: a page flip of one | |||
| 85 | + | // output (step with only set) keeps it while another output still animates. | |||
| 86 | + | func TestStepKeepsFallbackTimerWhileAnotherOutputAnimates(t *testing.T) { | |||
| 87 | + | c, ic := indicatorCore(t) | |||
| 88 | + | c.cfg.Animations.On = true | |||
| 89 | + | a := c.cur() | |||
| 90 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |||
| 91 | + | b := c.screens[1] | |||
| 92 | + | a.mon.AddWindow(1) | |||
| 93 | + | b.mon.AddWindow(2) | |||
| 94 | + | settleShown(t, c) | |||
| 95 | + | for _, sc := range []*screen{a, b} { | |||
| 96 | + | w := sc.mon.Current() | |||
| 97 | + | w.shift, w.motion = 100, c.spring(viewSpring(100, 0), ic.now) | |||
| 98 | + | } | |||
| 99 | + | c.armFrame() | |||
| 100 | + | armed := c.frameC | |||
| 101 | + | ic.now = ic.now.Add(16 * time.Millisecond) | |||
| 102 | + | if err := c.step(context.Background(), a); err != nil { | |||
| 103 | + | t.Fatal(err) | |||
| 104 | + | } | |||
| 105 | + | <-c.ch.Scenes | |||
| 106 | + | if c.frameC == nil || c.frameC != armed { | |||
| 107 | + | t.Fatal("a flip of A re-armed or dropped the fallback timer while B animates") | |||
| 108 | + | } | |||
| 109 | + | ||||
| 110 | + | // With nothing else animating the flip's own publish decides. | |||
| 111 | + | b.mon.Current().stopSlide() | |||
| 112 | + | if err := c.step(context.Background(), a); err != nil { | |||
| 113 | + | t.Fatal(err) | |||
| 114 | + | } | |||
| 115 | + | <-c.ch.Scenes | |||
| 116 | + | if c.frameC == nil || c.frameC == armed { | |||
| 117 | + | t.Fatal("A still animates but its timer was not re-armed") | |||
| 118 | + | } | |||
| 119 | + | ic.now = ic.now.Add(5 * time.Second) | |||
| 120 | + | if err := c.step(context.Background(), nil); err != nil { | |||
| 121 | + | t.Fatal(err) | |||
| 122 | + | } | |||
| 123 | + | <-c.ch.Scenes | |||
| 124 | + | if c.animating() || c.frameC != nil { | |||
| 125 | + | t.Fatalf("settled but the timer runs (%v)", c.frameC != nil) | |||
| 126 | + | } | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | // A window that left the layout while its rect motion ran drops the motion: | |||
| 130 | + | // it would ask for frames with nothing to move. | |||
| 131 | + | func TestRefreshShownDropsRectsOfWindowsOutOfLayout(t *testing.T) { | |||
| 132 | + | c, ic := indicatorCore(t) | |||
| 133 | + | c.cfg.Animations.On = true | |||
| 134 | + | sc := c.cur() | |||
| 135 | + | sc.mon.AddWindow(1) | |||
| 136 | + | sc.mon.AddWindow(2) | |||
| 137 | + | settleShown(t, c) | |||
| 138 | + | sp := viewSpring(40, 0) | |||
| 139 | + | sc.rects = map[WindowID]rectMotion{ | |||
| 140 | + | 1: {x: c.spring(sp, ic.now), dx: 40}, | |||
| 141 | + | 99: {x: c.spring(sp, ic.now), dx: 40}, | |||
| 142 | + | } | |||
| 143 | + | sc.rectsWS = sc.mon.Current() | |||
| 144 | + | settleShown(t, c) | |||
| 145 | + | if _, ok := sc.rects[99]; ok { | |||
| 146 | + | t.Fatal("a window outside the layout keeps its rect motion") | |||
| 147 | + | } | |||
| 148 | + | if _, ok := sc.rects[1]; !ok { | |||
| 149 | + | t.Fatal("a window in the layout lost its rect motion") | |||
| 150 | + | } | |||
| 151 | + | } | |||
| 152 | + | ||||
| 153 | + | // A swipe that follows the fingers owns the view: a focus bind pressed | |||
| 154 | + | // meanwhile starts no camera motion. | |||
| 155 | + | func TestNoCameraTransitionWhileSwipeFollows(t *testing.T) { | |||
| 156 | + | c, ic := indicatorCore(t) | |||
| 157 | + | c.cfg.Animations.On = true | |||
| 158 | + | sc := c.cur() | |||
| 159 | + | for id := WindowID(1); id <= 4; id++ { | |||
| 160 | + | sc.mon.AddWindow(id) | |||
| 161 | + | } | |||
| 162 | + | settleShown(t, c) | |||
| 163 | + | w := sc.mon.Current() | |||
| 164 | + | w.shift = 25 | |||
| 165 | + | c.swipe = &swipeGesture{screen: sc, ws: w, mode: swipeColumns} | |||
| 166 | + | before := c.snapshot() | |||
| 167 | + | viewX := w.ViewX | |||
| 168 | + | for range 3 { | |||
| 169 | + | c.applyAction(ActionFocusColumnLeft) | |||
| 170 | + | } | |||
| 171 | + | if w.ViewX == viewX { | |||
| 172 | + | t.Fatal("the focus did not move the view: the case checks nothing") | |||
| 173 | + | } | |||
| 174 | + | c.transition(before, ic.now) | |||
| 175 | + | if w.motion.on || w.shift != 25 { | |||
| 176 | + | t.Fatalf("camera motion %v, shift %v: the transition took over the fingers' view", w.motion.on, w.shift) | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | // A switch to or from a workspace with its own size stays settled: no | |||
| 181 | + | // monitor-wide slide would be cropped to it. The same switch between | |||
| 182 | + | // workspaces of the monitor's size slides. | |||
| 183 | + | func TestNoSwitchMotionForSizedWorkspace(t *testing.T) { | |||
| 184 | + | for _, tc := range []struct { | |||
| 185 | + | name string | |||
| 186 | + | sized bool | |||
| 187 | + | }{{"sized", true}, {"unsized", false}} { | |||
| 188 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 189 | + | c, ic := indicatorCore(t) | |||
| 190 | + | c.cfg.Animations.On = true | |||
| 191 | + | m := c.cur().mon | |||
| 192 | + | m.AddWindow(1) | |||
| 193 | + | if tc.sized { | |||
| 194 | + | m.Workspaces[1].SetSize(100, 100) | |||
| 195 | + | } | |||
| 196 | + | settleShown(t, c) | |||
| 197 | + | before := c.snapshot() | |||
| 198 | + | m.Focus(1) | |||
| 199 | + | c.transition(before, ic.now) | |||
| 200 | + | if got := m.switchMotion.on; got == tc.sized { | |||
| 201 | + | t.Fatalf("switch motion %v with a sized workspace %v", got, tc.sized) | |||
| 202 | + | } | |||
| 203 | + | if tc.sized && m.switchOff != 0 { | |||
| 204 | + | t.Fatalf("switchOff %v left for a framed switch", m.switchOff) | |||
| 205 | + | } | |||
| 206 | + | }) | |||
| 207 | + | } | |||
| 208 | + | } | |||
D internal/core/transition_internal_test.go +0 -44
| @@ -1,44 +0,0 @@ | |||
| 1 | - | package core | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "testing" | |
| 5 | - | "time" | |
| 6 | - | ) | |
| 7 | - | ||
| 8 | - | // The focus pulse waits while a camera transition of a key runs, and starts | |
| 9 | - | // once it settled. | |
| 10 | - | func TestPulseWaitsForCameraTransition(t *testing.T) { | |
| 11 | - | c, ic := pulseCore(t) | |
| 12 | - | m := c.cur().mon | |
| 13 | - | for id := WindowID(3); id <= 4; id++ { | |
| 14 | - | m.AddWindow(id) | |
| 15 | - | } | |
| 16 | - | indicatorScene(t, c) | |
| 17 | - | ic.now = ic.now.Add(pulseSettle + pulseCooldown) | |
| 18 | - | c.pulseTick() | |
| 19 | - | indicatorScene(t, c) | |
| 20 | - | ||
| 21 | - | before := c.snapshot() | |
| 22 | - | for range 3 { | |
| 23 | - | c.applyAction(ActionFocusColumnLeft) | |
| 24 | - | } | |
| 25 | - | c.transition(before, ic.now) | |
| 26 | - | w := m.Current() | |
| 27 | - | if !w.motion.on || m.settled() { | |
| 28 | - | t.Fatal("no camera transition after the focus moved the view") | |
| 29 | - | } | |
| 30 | - | indicatorScene(t, c) | |
| 31 | - | ic.now = ic.now.Add(pulseSettle) | |
| 32 | - | if c.pulseTick() { | |
| 33 | - | t.Fatal("pulse started while the view slides") | |
| 34 | - | } | |
| 35 | - | c.animate(ic.now.Add(5*time.Second), nil) | |
| 36 | - | ic.now = ic.now.Add(5 * time.Second) | |
| 37 | - | if !m.settled() { | |
| 38 | - | t.Fatal("view still slides") | |
| 39 | - | } | |
| 40 | - | ic.now = ic.now.Add(pulseSettle) | |
| 41 | - | if !c.pulseTick() { | |
| 42 | - | t.Fatal("no pulse once the view settled") | |
| 43 | - | } | |
| 44 | - | } | |
| @@ -1,44 +0,0 @@ | |||||
| 1 | - | package core | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "testing" | |||
| 5 | - | "time" | |||
| 6 | - | ) | |||
| 7 | - | ||||
| 8 | - | // The focus pulse waits while a camera transition of a key runs, and starts | |||
| 9 | - | // once it settled. | |||
| 10 | - | func TestPulseWaitsForCameraTransition(t *testing.T) { | |||
| 11 | - | c, ic := pulseCore(t) | |||
| 12 | - | m := c.cur().mon | |||
| 13 | - | for id := WindowID(3); id <= 4; id++ { | |||
| 14 | - | m.AddWindow(id) | |||
| 15 | - | } | |||
| 16 | - | indicatorScene(t, c) | |||
| 17 | - | ic.now = ic.now.Add(pulseSettle + pulseCooldown) | |||
| 18 | - | c.pulseTick() | |||
| 19 | - | indicatorScene(t, c) | |||
| 20 | - | ||||
| 21 | - | before := c.snapshot() | |||
| 22 | - | for range 3 { | |||
| 23 | - | c.applyAction(ActionFocusColumnLeft) | |||
| 24 | - | } | |||
| 25 | - | c.transition(before, ic.now) | |||
| 26 | - | w := m.Current() | |||
| 27 | - | if !w.motion.on || m.settled() { | |||
| 28 | - | t.Fatal("no camera transition after the focus moved the view") | |||
| 29 | - | } | |||
| 30 | - | indicatorScene(t, c) | |||
| 31 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 32 | - | if c.pulseTick() { | |||
| 33 | - | t.Fatal("pulse started while the view slides") | |||
| 34 | - | } | |||
| 35 | - | c.animate(ic.now.Add(5*time.Second), nil) | |||
| 36 | - | ic.now = ic.now.Add(5 * time.Second) | |||
| 37 | - | if !m.settled() { | |||
| 38 | - | t.Fatal("view still slides") | |||
| 39 | - | } | |||
| 40 | - | ic.now = ic.now.Add(pulseSettle) | |||
| 41 | - | if !c.pulseTick() { | |||
| 42 | - | t.Fatal("no pulse once the view settled") | |||
| 43 | - | } | |||
| 44 | - | } | |||
M internal/core/transition_test.go +93 -29
| @@ -129,22 +129,47 @@ func TestAnimationsOffKeysLandAtOnce(t *testing.T) { | |||
| 129 | 129 | r.noFrameScene(t) | |
| 130 | 130 | } | |
| 131 | 131 | ||
| 132 | - | // With the overview open before or after, nothing slides. | |
| 132 | + | // With the overview open before or after, nothing slides: opening it on a | |
| 133 | + | // scrolled view starts no motion, and neither does a bind that moves the view | |
| 134 | + | // while it is open. | |
| 133 | 135 | func TestNoTransitionWithOverview(t *testing.T) { | |
| 134 | - | r := startSwipe(t, nil) | |
| 135 | - | r.workspaces(t) | |
| 136 | - | r.key(t, "o", ports.ModAlt) | |
| 137 | - | r.key(t, "o", ports.ModAlt) | |
| 138 | - | r.noFrameScene(t) | |
| 136 | + | for _, tc := range []struct { | |
| 137 | + | name string | |
| 138 | + | // open is what the overview shows before the keys of the bind. | |
| 139 | + | sym string | |
| 140 | + | mods ports.Mods | |
| 141 | + | }{ | |
| 142 | + | {"open and close", "", 0}, | |
| 143 | + | {"focus bind while open", "Left", ports.ModAlt}, | |
| 144 | + | {"workspace bind while open", "Next", ports.ModAlt}, | |
| 145 | + | } { | |
| 146 | + | t.Run(tc.name, func(t *testing.T) { | |
| 147 | + | r := startSwipe(t, nil) | |
| 148 | + | fourColumns(t, r) | |
| 149 | + | r.key(t, "Right", ports.ModAlt) // 2 is on screen: no scroll | |
| 150 | + | r.keySettled(t, "Right") // the view scrolls away from 0 | |
| 151 | + | r.keySettled(t, "Right") | |
| 152 | + | r.key(t, "o", ports.ModAlt) | |
| 153 | + | r.noFrameScene(t) | |
| 154 | + | if tc.sym != "" { | |
| 155 | + | r.key(t, tc.sym, tc.mods) | |
| 156 | + | r.noFrameScene(t) | |
| 157 | + | } | |
| 158 | + | r.key(t, "o", ports.ModAlt) | |
| 159 | + | r.noFrameScene(t) | |
| 160 | + | }) | |
| 161 | + | } | |
| 139 | 162 | } | |
| 140 | 163 | ||
| 141 | - | // A workspace that goes to another monitor gets no motion on either side. | |
| 164 | + | // A workspace that goes to another monitor gets no rect or switch motion on | |
| 165 | + | // either side. | |
| 142 | 166 | func TestMovedWorkspaceHasNoMotion(t *testing.T) { | |
| 143 | 167 | r := startSwipe(t, nil) | |
| 144 | - | r.mapWindow(t, 1) | |
| 145 | - | r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}) | |
| 168 | + | twoColumns(t, r) | |
| 169 | + | other := ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000} | |
| 170 | + | r.plug(t, other) | |
| 146 | 171 | r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |
| 147 | - | r.noFrameScene(t) | |
| 172 | + | r.noFrameScene(t, wide.Name, other.Name) | |
| 148 | 173 | } | |
| 149 | 174 | ||
| 150 | 175 | // rectNow is the rect of window id in s; it must be in the scene. | |
| @@ -309,25 +334,6 @@ func TestFullscreenRectTransition(t *testing.T) { | |||
| 309 | 334 | } | |
| 310 | 335 | } | |
| 311 | 336 | ||
| 312 | - | // A workspace that moves to another monitor gets no rect or switch motion | |
| 313 | - | // on either monitor. | |
| 314 | - | func TestMovedWorkspaceGetsNoRectMotion(t *testing.T) { | |
| 315 | - | r := startSwipe(t, nil) | |
| 316 | - | twoColumns(t, r) | |
| 317 | - | other := ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000} | |
| 318 | - | r.plug(t, other) | |
| 319 | - | r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |
| 320 | - | for _, o := range []string{wide.Name, other.Name} { | |
| 321 | - | r.advance(16 * time.Millisecond) | |
| 322 | - | r.frames <- ports.OutputFrame{Output: o} | |
| 323 | - | } | |
| 324 | - | select { | |
| 325 | - | case s := <-r.scenes: | |
| 326 | - | t.Fatalf("a flip published a scene (%d outputs): a motion runs", len(s)) | |
| 327 | - | case <-time.After(50 * time.Millisecond): | |
| 328 | - | } | |
| 329 | - | } | |
| 330 | - | ||
| 331 | 337 | // With animations off a layout change lands at once. | |
| 332 | 338 | func TestAnimationsOffRectsLandAtOnce(t *testing.T) { | |
| 333 | 339 | r := startSwipe(t, animationsOff) | |
| @@ -357,3 +363,61 @@ func TestChainedMovesStartFromShownRects(t *testing.T) { | |||
| 357 | 363 | t.Fatalf("settled 1 at %+v and 2 at %+v, want 0 and 400", got1, got2) | |
| 358 | 364 | } | |
| 359 | 365 | } | |
| 366 | + | ||
| 367 | + | // A click on a column that is partly off screen focuses it: the scene right | |
| 368 | + | // after the click still shows the old view, a frame an intermediate one and | |
| 369 | + | // the settled scene the new one. | |
| 370 | + | func TestClickFocusTransition(t *testing.T) { | |
| 371 | + | r := startSwipe(t, func(c *ports.Config) { c.Binds["Cmd+Ctrl+Right"] = "set-column-width +10%" }) | |
| 372 | + | threeColumns(t, r) | |
| 373 | + | // Column 3 grows to 480: column 2 sticks out 80 px on the left. | |
| 374 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 375 | + | if got := xOf(t, r.settle(t), 2); got != -80 { | |
| 376 | + | t.Fatalf("column 2 at %d, want -80", got) | |
| 377 | + | } | |
| 378 | + | // The release of the modifiers: a plain click, not a window drag. | |
| 379 | + | r.input <- ports.KeyEvent{Keysym: "Alt_L"} | |
| 380 | + | r.input <- ports.PointerMotion{X: 100, Y: 300} | |
| 381 | + | r.drain() | |
| 382 | + | r.input <- ports.PointerButton{Button: 0x110, Pressed: true} | |
| 383 | + | s := scene(t, r.scenes) | |
| 384 | + | if got := xOf(t, s, 2); got != -80 { | |
| 385 | + | t.Fatalf("column 2 at %d right after the click, want the old -80", got) | |
| 386 | + | } | |
| 387 | + | got := xOf(t, r.frame(t, 16*time.Millisecond), 2) | |
| 388 | + | if got <= -80 || got >= 0 { | |
| 389 | + | t.Fatalf("column 2 at %d on the first frame, want between -80 and 0", got) | |
| 390 | + | } | |
| 391 | + | if got := xOf(t, r.settle(t), 2); got != 0 { | |
| 392 | + | t.Fatalf("column 2 at %d settled, want 0", got) | |
| 393 | + | } | |
| 394 | + | } | |
| 395 | + | ||
| 396 | + | // A discrete swipe step (fixed overflow: nothing follows the fingers) moves | |
| 397 | + | // through a rect motion: the scene right after the lift shows the old rects. | |
| 398 | + | func TestDiscreteSwipeTransition(t *testing.T) { | |
| 399 | + | r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" }) | |
| 400 | + | r.mapWindow(t, 1) | |
| 401 | + | r.mapWindow(t, 2) | |
| 402 | + | r.key(t, "f", ports.ModAlt) // maximize window 2 | |
| 403 | + | if got := rectNow(t, r.settle(t), 2); got.W != 800 { | |
| 404 | + | t.Fatalf("window 2 is %d wide, want maximized 800", got.W) | |
| 405 | + | } | |
| 406 | + | r.begin() | |
| 407 | + | for range 10 { | |
| 408 | + | r.at += 8 * time.Millisecond | |
| 409 | + | r.input <- ports.SwipeUpdate{DX: -40, Time: r.at} | |
| 410 | + | } | |
| 411 | + | s := r.end(t, false) | |
| 412 | + | if got := rectNow(t, s, 2); got.W != 800 { | |
| 413 | + | t.Fatalf("window 2 is %d wide right after the lift, want the old 800", got.W) | |
| 414 | + | } | |
| 415 | + | got := rectNow(t, r.frame(t, 16*time.Millisecond), 2) | |
| 416 | + | if got.W <= 400 || got.W >= 800 { | |
| 417 | + | t.Fatalf("window 2 is %d wide on the first frame, want between 400 and 800", got.W) | |
| 418 | + | } | |
| 419 | + | s = r.settle(t) | |
| 420 | + | if got1, got2 := rectNow(t, s, 1), rectNow(t, s, 2); got1.W != 400 || got2.X != 400 || got2.W != 400 { | |
| 421 | + | t.Fatalf("settled 1 at %+v and 2 at %+v, want 1 beside 2 on halves", got1, got2) | |
| 422 | + | } | |
| 423 | + | } | |
| @@ -129,22 +129,47 @@ func TestAnimationsOffKeysLandAtOnce(t *testing.T) { | |||||
| 129 | r.noFrameScene(t) | 129 | r.noFrameScene(t) | ||
| 130 | } | 130 | } | ||
| 131 | 131 | ||||
| 132 | - | // With the overview open before or after, nothing slides. | 132 | + | // With the overview open before or after, nothing slides: opening it on a |
| 133 | + | // scrolled view starts no motion, and neither does a bind that moves the view | |||
| 134 | + | // while it is open. | |||
| 133 | func TestNoTransitionWithOverview(t *testing.T) { | 135 | func TestNoTransitionWithOverview(t *testing.T) { | ||
| 134 | - | r := startSwipe(t, nil) | 136 | + | for _, tc := range []struct { |
| 135 | - | r.workspaces(t) | 137 | + | name string |
| 136 | - | r.key(t, "o", ports.ModAlt) | 138 | + | // open is what the overview shows before the keys of the bind. |
| 137 | - | r.key(t, "o", ports.ModAlt) | 139 | + | sym string |
| 138 | - | r.noFrameScene(t) | 140 | + | mods ports.Mods |
| 141 | + | }{ | |||
| 142 | + | {"open and close", "", 0}, | |||
| 143 | + | {"focus bind while open", "Left", ports.ModAlt}, | |||
| 144 | + | {"workspace bind while open", "Next", ports.ModAlt}, | |||
| 145 | + | } { | |||
| 146 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 147 | + | r := startSwipe(t, nil) | |||
| 148 | + | fourColumns(t, r) | |||
| 149 | + | r.key(t, "Right", ports.ModAlt) // 2 is on screen: no scroll | |||
| 150 | + | r.keySettled(t, "Right") // the view scrolls away from 0 | |||
| 151 | + | r.keySettled(t, "Right") | |||
| 152 | + | r.key(t, "o", ports.ModAlt) | |||
| 153 | + | r.noFrameScene(t) | |||
| 154 | + | if tc.sym != "" { | |||
| 155 | + | r.key(t, tc.sym, tc.mods) | |||
| 156 | + | r.noFrameScene(t) | |||
| 157 | + | } | |||
| 158 | + | r.key(t, "o", ports.ModAlt) | |||
| 159 | + | r.noFrameScene(t) | |||
| 160 | + | }) | |||
| 161 | + | } | |||
| 139 | } | 162 | } | ||
| 140 | 163 | ||||
| 141 | - | // A workspace that goes to another monitor gets no motion on either side. | 164 | + | // A workspace that goes to another monitor gets no rect or switch motion on |
| 165 | + | // either side. | |||
| 142 | func TestMovedWorkspaceHasNoMotion(t *testing.T) { | 166 | func TestMovedWorkspaceHasNoMotion(t *testing.T) { | ||
| 143 | r := startSwipe(t, nil) | 167 | r := startSwipe(t, nil) | ||
| 144 | - | r.mapWindow(t, 1) | 168 | + | twoColumns(t, r) |
| 145 | - | r.plug(t, ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000}) | 169 | + | other := ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000} |
| 170 | + | r.plug(t, other) | |||
| 146 | r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | 171 | r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | ||
| 147 | - | r.noFrameScene(t) | 172 | + | r.noFrameScene(t, wide.Name, other.Name) |
| 148 | } | 173 | } | ||
| 149 | 174 | ||||
| 150 | // rectNow is the rect of window id in s; it must be in the scene. | 175 | // rectNow is the rect of window id in s; it must be in the scene. | ||
| @@ -309,25 +334,6 @@ func TestFullscreenRectTransition(t *testing.T) { | |||||
| 309 | } | 334 | } | ||
| 310 | } | 335 | } | ||
| 311 | 336 | ||||
| 312 | - | // A workspace that moves to another monitor gets no rect or switch motion | |||
| 313 | - | // on either monitor. | |||
| 314 | - | func TestMovedWorkspaceGetsNoRectMotion(t *testing.T) { | |||
| 315 | - | r := startSwipe(t, nil) | |||
| 316 | - | twoColumns(t, r) | |||
| 317 | - | other := ports.OutputInfo{Name: "DP-2", Width: 800, Height: 600, RefreshMilli: 60000} | |||
| 318 | - | r.plug(t, other) | |||
| 319 | - | r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |||
| 320 | - | for _, o := range []string{wide.Name, other.Name} { | |||
| 321 | - | r.advance(16 * time.Millisecond) | |||
| 322 | - | r.frames <- ports.OutputFrame{Output: o} | |||
| 323 | - | } | |||
| 324 | - | select { | |||
| 325 | - | case s := <-r.scenes: | |||
| 326 | - | t.Fatalf("a flip published a scene (%d outputs): a motion runs", len(s)) | |||
| 327 | - | case <-time.After(50 * time.Millisecond): | |||
| 328 | - | } | |||
| 329 | - | } | |||
| 330 | - | ||||
| 331 | // With animations off a layout change lands at once. | 337 | // With animations off a layout change lands at once. | ||
| 332 | func TestAnimationsOffRectsLandAtOnce(t *testing.T) { | 338 | func TestAnimationsOffRectsLandAtOnce(t *testing.T) { | ||
| 333 | r := startSwipe(t, animationsOff) | 339 | r := startSwipe(t, animationsOff) | ||
| @@ -357,3 +363,61 @@ func TestChainedMovesStartFromShownRects(t *testing.T) { | |||||
| 357 | t.Fatalf("settled 1 at %+v and 2 at %+v, want 0 and 400", got1, got2) | 363 | t.Fatalf("settled 1 at %+v and 2 at %+v, want 0 and 400", got1, got2) | ||
| 358 | } | 364 | } | ||
| 359 | } | 365 | } | ||
| 366 | + | ||||
| 367 | + | // A click on a column that is partly off screen focuses it: the scene right | |||
| 368 | + | // after the click still shows the old view, a frame an intermediate one and | |||
| 369 | + | // the settled scene the new one. | |||
| 370 | + | func TestClickFocusTransition(t *testing.T) { | |||
| 371 | + | r := startSwipe(t, func(c *ports.Config) { c.Binds["Cmd+Ctrl+Right"] = "set-column-width +10%" }) | |||
| 372 | + | threeColumns(t, r) | |||
| 373 | + | // Column 3 grows to 480: column 2 sticks out 80 px on the left. | |||
| 374 | + | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |||
| 375 | + | if got := xOf(t, r.settle(t), 2); got != -80 { | |||
| 376 | + | t.Fatalf("column 2 at %d, want -80", got) | |||
| 377 | + | } | |||
| 378 | + | // The release of the modifiers: a plain click, not a window drag. | |||
| 379 | + | r.input <- ports.KeyEvent{Keysym: "Alt_L"} | |||
| 380 | + | r.input <- ports.PointerMotion{X: 100, Y: 300} | |||
| 381 | + | r.drain() | |||
| 382 | + | r.input <- ports.PointerButton{Button: 0x110, Pressed: true} | |||
| 383 | + | s := scene(t, r.scenes) | |||
| 384 | + | if got := xOf(t, s, 2); got != -80 { | |||
| 385 | + | t.Fatalf("column 2 at %d right after the click, want the old -80", got) | |||
| 386 | + | } | |||
| 387 | + | got := xOf(t, r.frame(t, 16*time.Millisecond), 2) | |||
| 388 | + | if got <= -80 || got >= 0 { | |||
| 389 | + | t.Fatalf("column 2 at %d on the first frame, want between -80 and 0", got) | |||
| 390 | + | } | |||
| 391 | + | if got := xOf(t, r.settle(t), 2); got != 0 { | |||
| 392 | + | t.Fatalf("column 2 at %d settled, want 0", got) | |||
| 393 | + | } | |||
| 394 | + | } | |||
| 395 | + | ||||
| 396 | + | // A discrete swipe step (fixed overflow: nothing follows the fingers) moves | |||
| 397 | + | // through a rect motion: the scene right after the lift shows the old rects. | |||
| 398 | + | func TestDiscreteSwipeTransition(t *testing.T) { | |||
| 399 | + | r := startSwipe(t, func(c *ports.Config) { c.Layout.Overflow = "fixed" }) | |||
| 400 | + | r.mapWindow(t, 1) | |||
| 401 | + | r.mapWindow(t, 2) | |||
| 402 | + | r.key(t, "f", ports.ModAlt) // maximize window 2 | |||
| 403 | + | if got := rectNow(t, r.settle(t), 2); got.W != 800 { | |||
| 404 | + | t.Fatalf("window 2 is %d wide, want maximized 800", got.W) | |||
| 405 | + | } | |||
| 406 | + | r.begin() | |||
| 407 | + | for range 10 { | |||
| 408 | + | r.at += 8 * time.Millisecond | |||
| 409 | + | r.input <- ports.SwipeUpdate{DX: -40, Time: r.at} | |||
| 410 | + | } | |||
| 411 | + | s := r.end(t, false) | |||
| 412 | + | if got := rectNow(t, s, 2); got.W != 800 { | |||
| 413 | + | t.Fatalf("window 2 is %d wide right after the lift, want the old 800", got.W) | |||
| 414 | + | } | |||
| 415 | + | got := rectNow(t, r.frame(t, 16*time.Millisecond), 2) | |||
| 416 | + | if got.W <= 400 || got.W >= 800 { | |||
| 417 | + | t.Fatalf("window 2 is %d wide on the first frame, want between 400 and 800", got.W) | |||
| 418 | + | } | |||
| 419 | + | s = r.settle(t) | |||
| 420 | + | if got1, got2 := rectNow(t, s, 1), rectNow(t, s, 2); got1.W != 400 || got2.X != 400 || got2.W != 400 { | |||
| 421 | + | t.Fatalf("settled 1 at %+v and 2 at %+v, want 1 beside 2 on halves", got1, got2) | |||
| 422 | + | } | |||
| 423 | + | } | |||