test(core): animations-on variants of the border and old animations-off tests
Border scene invariants with animations on (weak invariants mid-transition, full ones once settled), On variants that land every spring on a mock clock for the workspace, drag, state, unplug, free float and workspace-size tests, and an unmap mid rect motion test.
- commit
- 431292a4d8d8a1e1239c04ef65b628a3fbf74daa
- author
- brice · · 2026-10-04 20:34 UTC
- parent
- b6a8e33
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +613 -176
M internal/core/border_invariants_test.go +136 -33
| @@ -2,7 +2,9 @@ package core_test | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "math/rand" | |
| 5 | + | "slices" | |
| 5 | 6 | "testing" | |
| 7 | + | "time" | |
| 6 | 8 | ||
| 7 | 9 | "github.com/bnema/neferwl/internal/ports" | |
| 8 | 10 | ) | |
| @@ -13,7 +15,6 @@ func TestBorderSceneInvariants(t *testing.T) { | |||
| 13 | 15 | for _, overflow := range []string{"fixed", "scroll"} { | |
| 14 | 16 | for _, gap := range []int{0, 6} { | |
| 15 | 17 | t.Run(overflow+map[int]string{0: "-flush", 6: "-gaps"}[gap], func(t *testing.T) { | |
| 16 | - | rng := rand.New(rand.NewSource(20260928)) | |
| 17 | 18 | r := startMulti(t, func(c *ports.Config) { | |
| 18 | 19 | c.Animations.On = false | |
| 19 | 20 | c.Border.Width = 2 | |
| @@ -21,45 +22,147 @@ func TestBorderSceneInvariants(t *testing.T) { | |||
| 21 | 22 | c.Layout.Overflow = overflow | |
| 22 | 23 | c.Layout.Gaps = gap | |
| 23 | 24 | }, left, right) | |
| 24 | - | var ids []ports.WindowID | |
| 25 | - | next := ports.WindowID(1) | |
| 26 | - | // A single keypress may publish a scene even when the action is | |
| 27 | - | // inapplicable; check every resulting scene independently. | |
| 28 | - | keys := []struct { | |
| 29 | - | sym string | |
| 30 | - | mods ports.Mods | |
| 31 | - | }{ | |
| 32 | - | {"Left", ports.ModAlt | ports.ModShift}, {"Right", ports.ModAlt | ports.ModShift}, | |
| 33 | - | {"Left", ports.ModAlt}, {"Right", ports.ModAlt}, | |
| 34 | - | {"Left", ports.ModAlt | ports.ModCtrl}, {"Right", ports.ModAlt | ports.ModCtrl}, | |
| 35 | - | {"Left", ports.ModAlt | ports.ModCtrl | ports.ModShift}, {"Right", ports.ModAlt | ports.ModCtrl | ports.ModShift}, | |
| 36 | - | {"bracketleft", ports.ModAlt}, {"bracketright", ports.ModAlt}, | |
| 37 | - | {"f", ports.ModAlt}, {"f", ports.ModAlt | ports.ModShift}, | |
| 38 | - | {"s", ports.ModAlt}, {"S", ports.ModAlt | ports.ModShift}, {"Up", ports.ModAlt}, {"Down", ports.ModAlt}, | |
| 39 | - | } | |
| 40 | - | for step := range 180 { | |
| 41 | - | var scenes []ports.Scene | |
| 42 | - | switch { | |
| 43 | - | case len(ids) < 2 || rng.Intn(5) == 0 && len(ids) < 12: | |
| 44 | - | scenes = r.mapWindow(t, next) | |
| 45 | - | ids = append(ids, next) | |
| 46 | - | next++ | |
| 47 | - | case rng.Intn(9) == 0: | |
| 48 | - | i := rng.Intn(len(ids)) | |
| 49 | - | r.client <- ports.WindowUnmapped{ID: ids[i]} | |
| 50 | - | scenes = receive(t, r.scenes) | |
| 51 | - | ids = append(ids[:i], ids[i+1:]...) | |
| 52 | - | default: | |
| 53 | - | k := keys[rng.Intn(len(keys))] | |
| 54 | - | scenes = r.key(t, k.sym, k.mods) | |
| 25 | + | borderScenario(t, r, gap, nil) | |
| 26 | + | }) | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // With animations on, the scene right after an action shows the old layout | |
| 32 | + | // moving: only the weak invariants hold there. Once every spring settled | |
| 33 | + | // (the test moves the clock and sends the page flips) the full ones do, | |
| 34 | + | // whatever the actions that ran mid-transition. Insets and lit lines come | |
| 35 | + | // from the settled topology by design: separators follow the shown layout. | |
| 36 | + | func TestBorderSceneInvariantsAnimated(t *testing.T) { | |
| 37 | + | for _, overflow := range []string{"fixed", "scroll"} { | |
| 38 | + | for _, gap := range []int{0, 6} { | |
| 39 | + | t.Run(overflow+map[int]string{0: "-flush", 6: "-gaps"}[gap], func(t *testing.T) { | |
| 40 | + | r := startSwipeOn(t, func(c *ports.Config) { | |
| 41 | + | c.Animations.On = true | |
| 42 | + | c.Border.Width = 2 | |
| 43 | + | c.Layout.MaxColumns = 3 | |
| 44 | + | c.Layout.Overflow = overflow | |
| 45 | + | c.Layout.Gaps = gap | |
| 46 | + | }, left, right) | |
| 47 | + | moved := 0 | |
| 48 | + | borderScenario(t, r.multiRig, gap, func(set []ports.Scene) []ports.Scene { | |
| 49 | + | settled, ok := r.settleAll(t, left.Name, right.Name) | |
| 50 | + | if !ok { | |
| 51 | + | return set | |
| 52 | + | } | |
| 53 | + | if !sameRects(set, settled) { | |
| 54 | + | moved++ | |
| 55 | 55 | } | |
| 56 | - | checkBorderScenes(t, step, gap, scenes) | |
| 56 | + | // Settled means no spring runs: a further flip has nothing to do. | |
| 57 | + | if _, again := r.flipAll(t, 16*time.Millisecond, left.Name, right.Name); again { | |
| 58 | + | t.Fatal("a flip after the settle still published: a spring runs") | |
| 59 | + | } | |
| 60 | + | return settled | |
| 61 | + | }) | |
| 62 | + | if moved == 0 { | |
| 63 | + | t.Fatal("no scene was ever mid-transition: the case checks nothing") | |
| 57 | 64 | } | |
| 65 | + | t.Logf("%d steps checked mid-transition and settled", moved) | |
| 58 | 66 | }) | |
| 59 | 67 | } | |
| 60 | 68 | } | |
| 61 | 69 | } | |
| 62 | 70 | ||
| 71 | + | // borderScenario drives 180 random steps and checks every scene set. With | |
| 72 | + | // settle set the steps are animated: the set a step publishes is checked | |
| 73 | + | // with the weak invariants, then the settled set (settle returns the set | |
| 74 | + | // itself when nothing animated) with the full ones. One step in four skips | |
| 75 | + | // the settling, so the next action starts mid-transition; the last step | |
| 76 | + | // always settles. | |
| 77 | + | func borderScenario(t *testing.T, r *multiRig, gap int, settle func([]ports.Scene) []ports.Scene) { | |
| 78 | + | t.Helper() | |
| 79 | + | rng := rand.New(rand.NewSource(20260928)) | |
| 80 | + | var ids []ports.WindowID | |
| 81 | + | next := ports.WindowID(1) | |
| 82 | + | // A single keypress may publish a scene even when the action is | |
| 83 | + | // inapplicable; check every resulting scene independently. | |
| 84 | + | keys := []struct { | |
| 85 | + | sym string | |
| 86 | + | mods ports.Mods | |
| 87 | + | }{ | |
| 88 | + | {"Left", ports.ModAlt | ports.ModShift}, {"Right", ports.ModAlt | ports.ModShift}, | |
| 89 | + | {"Left", ports.ModAlt}, {"Right", ports.ModAlt}, | |
| 90 | + | {"Left", ports.ModAlt | ports.ModCtrl}, {"Right", ports.ModAlt | ports.ModCtrl}, | |
| 91 | + | {"Left", ports.ModAlt | ports.ModCtrl | ports.ModShift}, {"Right", ports.ModAlt | ports.ModCtrl | ports.ModShift}, | |
| 92 | + | {"bracketleft", ports.ModAlt}, {"bracketright", ports.ModAlt}, | |
| 93 | + | {"f", ports.ModAlt}, {"f", ports.ModAlt | ports.ModShift}, | |
| 94 | + | {"s", ports.ModAlt}, {"S", ports.ModAlt | ports.ModShift}, {"Up", ports.ModAlt}, {"Down", ports.ModAlt}, | |
| 95 | + | } | |
| 96 | + | const steps = 180 | |
| 97 | + | for step := range steps { | |
| 98 | + | var scenes []ports.Scene | |
| 99 | + | switch { | |
| 100 | + | case len(ids) < 2 || rng.Intn(5) == 0 && len(ids) < 12: | |
| 101 | + | scenes = r.mapWindow(t, next) | |
| 102 | + | ids = append(ids, next) | |
| 103 | + | next++ | |
| 104 | + | case rng.Intn(9) == 0: | |
| 105 | + | i := rng.Intn(len(ids)) | |
| 106 | + | r.client <- ports.WindowUnmapped{ID: ids[i]} | |
| 107 | + | scenes = receive(t, r.scenes) | |
| 108 | + | ids = append(ids[:i], ids[i+1:]...) | |
| 109 | + | default: | |
| 110 | + | k := keys[rng.Intn(len(keys))] | |
| 111 | + | scenes = r.key(t, k.sym, k.mods) | |
| 112 | + | } | |
| 113 | + | if settle == nil { | |
| 114 | + | checkBorderScenes(t, step, gap, scenes) | |
| 115 | + | continue | |
| 116 | + | } | |
| 117 | + | checkBorderScenesMoving(t, step, scenes) | |
| 118 | + | if rng.Intn(4) != 0 || step == steps-1 { | |
| 119 | + | checkBorderScenes(t, step, gap, settle(scenes)) | |
| 120 | + | } | |
| 121 | + | } | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // sameRects reports whether two scene sets draw every window at the same | |
| 125 | + | // rect. | |
| 126 | + | func sameRects(a, b []ports.Scene) bool { | |
| 127 | + | if len(a) != len(b) { | |
| 128 | + | return false | |
| 129 | + | } | |
| 130 | + | for i := range a { | |
| 131 | + | if !slices.EqualFunc(a[i].Windows, b[i].Windows, func(x, y ports.SceneWindow) bool { return x.ID == y.ID && x.Rect == y.Rect }) { | |
| 132 | + | return false | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | return true | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | // checkBorderScenesMoving is what holds for a scene published while | |
| 139 | + | // windows still move: every separator lies inside its output and belongs | |
| 140 | + | // to a window that is shown, and at most one window is focused. | |
| 141 | + | func checkBorderScenesMoving(t *testing.T, step int, scenes []ports.Scene) { | |
| 142 | + | t.Helper() | |
| 143 | + | focused := 0 | |
| 144 | + | for _, s := range scenes { | |
| 145 | + | o := ports.Rect{W: s.OutputWidth, H: s.OutputHeight} | |
| 146 | + | for _, sep := range s.Separators { | |
| 147 | + | r := sep.Rect | |
| 148 | + | if r.W <= 0 || r.H <= 0 || r.X < 0 || r.Y < 0 || r.X+r.W > o.W || r.Y+r.H > o.H { | |
| 149 | + | t.Fatalf("step %d %s (moving): line outside output: %+v; scene %+v", step, s.Output, sep, s) | |
| 150 | + | } | |
| 151 | + | if sep.Window != 0 && !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == sep.Window && !w.Hidden }) { | |
| 152 | + | t.Fatalf("step %d %s (moving): line belongs to a hidden or absent window: %+v; scene %+v", step, s.Output, sep, s) | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | for _, w := range s.Windows { | |
| 156 | + | if w.Focused && !w.Hidden && !w.Popup { | |
| 157 | + | focused++ | |
| 158 | + | } | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | if focused > 1 { | |
| 162 | + | t.Fatalf("step %d (moving): several focused windows: %+v", step, scenes) | |
| 163 | + | } | |
| 164 | + | } | |
| 165 | + | ||
| 63 | 166 | func checkBorderScenes(t *testing.T, step, gap int, scenes []ports.Scene) { | |
| 64 | 167 | t.Helper() | |
| 65 | 168 | focused := 0 | |
| @@ -2,7 +2,9 @@ package core_test | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "math/rand" | 4 | "math/rand" | ||
| 5 | + | "slices" | |||
| 5 | "testing" | 6 | "testing" | ||
| 7 | + | "time" | |||
| 6 | 8 | ||||
| 7 | "github.com/bnema/neferwl/internal/ports" | 9 | "github.com/bnema/neferwl/internal/ports" | ||
| 8 | ) | 10 | ) | ||
| @@ -13,7 +15,6 @@ func TestBorderSceneInvariants(t *testing.T) { | |||||
| 13 | for _, overflow := range []string{"fixed", "scroll"} { | 15 | for _, overflow := range []string{"fixed", "scroll"} { | ||
| 14 | for _, gap := range []int{0, 6} { | 16 | for _, gap := range []int{0, 6} { | ||
| 15 | t.Run(overflow+map[int]string{0: "-flush", 6: "-gaps"}[gap], func(t *testing.T) { | 17 | t.Run(overflow+map[int]string{0: "-flush", 6: "-gaps"}[gap], func(t *testing.T) { | ||
| 16 | - | rng := rand.New(rand.NewSource(20260928)) | |||
| 17 | r := startMulti(t, func(c *ports.Config) { | 18 | r := startMulti(t, func(c *ports.Config) { | ||
| 18 | c.Animations.On = false | 19 | c.Animations.On = false | ||
| 19 | c.Border.Width = 2 | 20 | c.Border.Width = 2 | ||
| @@ -21,45 +22,147 @@ func TestBorderSceneInvariants(t *testing.T) { | |||||
| 21 | c.Layout.Overflow = overflow | 22 | c.Layout.Overflow = overflow | ||
| 22 | c.Layout.Gaps = gap | 23 | c.Layout.Gaps = gap | ||
| 23 | }, left, right) | 24 | }, left, right) | ||
| 24 | - | var ids []ports.WindowID | 25 | + | borderScenario(t, r, gap, nil) |
| 25 | - | next := ports.WindowID(1) | 26 | + | }) |
| 26 | - | // A single keypress may publish a scene even when the action is | 27 | + | } |
| 27 | - | // inapplicable; check every resulting scene independently. | 28 | + | } |
| 28 | - | keys := []struct { | 29 | + | } |
| 29 | - | sym string | 30 | + | |
| 30 | - | mods ports.Mods | 31 | + | // With animations on, the scene right after an action shows the old layout |
| 31 | - | }{ | 32 | + | // moving: only the weak invariants hold there. Once every spring settled |
| 32 | - | {"Left", ports.ModAlt | ports.ModShift}, {"Right", ports.ModAlt | ports.ModShift}, | 33 | + | // (the test moves the clock and sends the page flips) the full ones do, |
| 33 | - | {"Left", ports.ModAlt}, {"Right", ports.ModAlt}, | 34 | + | // whatever the actions that ran mid-transition. Insets and lit lines come |
| 34 | - | {"Left", ports.ModAlt | ports.ModCtrl}, {"Right", ports.ModAlt | ports.ModCtrl}, | 35 | + | // from the settled topology by design: separators follow the shown layout. |
| 35 | - | {"Left", ports.ModAlt | ports.ModCtrl | ports.ModShift}, {"Right", ports.ModAlt | ports.ModCtrl | ports.ModShift}, | 36 | + | func TestBorderSceneInvariantsAnimated(t *testing.T) { |
| 36 | - | {"bracketleft", ports.ModAlt}, {"bracketright", ports.ModAlt}, | 37 | + | for _, overflow := range []string{"fixed", "scroll"} { |
| 37 | - | {"f", ports.ModAlt}, {"f", ports.ModAlt | ports.ModShift}, | 38 | + | for _, gap := range []int{0, 6} { |
| 38 | - | {"s", ports.ModAlt}, {"S", ports.ModAlt | ports.ModShift}, {"Up", ports.ModAlt}, {"Down", ports.ModAlt}, | 39 | + | t.Run(overflow+map[int]string{0: "-flush", 6: "-gaps"}[gap], func(t *testing.T) { |
| 39 | - | } | 40 | + | r := startSwipeOn(t, func(c *ports.Config) { |
| 40 | - | for step := range 180 { | 41 | + | c.Animations.On = true |
| 41 | - | var scenes []ports.Scene | 42 | + | c.Border.Width = 2 |
| 42 | - | switch { | 43 | + | c.Layout.MaxColumns = 3 |
| 43 | - | case len(ids) < 2 || rng.Intn(5) == 0 && len(ids) < 12: | 44 | + | c.Layout.Overflow = overflow |
| 44 | - | scenes = r.mapWindow(t, next) | 45 | + | c.Layout.Gaps = gap |
| 45 | - | ids = append(ids, next) | 46 | + | }, left, right) |
| 46 | - | next++ | 47 | + | moved := 0 |
| 47 | - | case rng.Intn(9) == 0: | 48 | + | borderScenario(t, r.multiRig, gap, func(set []ports.Scene) []ports.Scene { |
| 48 | - | i := rng.Intn(len(ids)) | 49 | + | settled, ok := r.settleAll(t, left.Name, right.Name) |
| 49 | - | r.client <- ports.WindowUnmapped{ID: ids[i]} | 50 | + | if !ok { |
| 50 | - | scenes = receive(t, r.scenes) | 51 | + | return set |
| 51 | - | ids = append(ids[:i], ids[i+1:]...) | 52 | + | } |
| 52 | - | default: | 53 | + | if !sameRects(set, settled) { |
| 53 | - | k := keys[rng.Intn(len(keys))] | 54 | + | moved++ |
| 54 | - | scenes = r.key(t, k.sym, k.mods) | |||
| 55 | } | 55 | } | ||
| 56 | - | checkBorderScenes(t, step, gap, scenes) | 56 | + | // Settled means no spring runs: a further flip has nothing to do. |
| 57 | + | if _, again := r.flipAll(t, 16*time.Millisecond, left.Name, right.Name); again { | |||
| 58 | + | t.Fatal("a flip after the settle still published: a spring runs") | |||
| 59 | + | } | |||
| 60 | + | return settled | |||
| 61 | + | }) | |||
| 62 | + | if moved == 0 { | |||
| 63 | + | t.Fatal("no scene was ever mid-transition: the case checks nothing") | |||
| 57 | } | 64 | } | ||
| 65 | + | t.Logf("%d steps checked mid-transition and settled", moved) | |||
| 58 | }) | 66 | }) | ||
| 59 | } | 67 | } | ||
| 60 | } | 68 | } | ||
| 61 | } | 69 | } | ||
| 62 | 70 | ||||
| 71 | + | // borderScenario drives 180 random steps and checks every scene set. With | |||
| 72 | + | // settle set the steps are animated: the set a step publishes is checked | |||
| 73 | + | // with the weak invariants, then the settled set (settle returns the set | |||
| 74 | + | // itself when nothing animated) with the full ones. One step in four skips | |||
| 75 | + | // the settling, so the next action starts mid-transition; the last step | |||
| 76 | + | // always settles. | |||
| 77 | + | func borderScenario(t *testing.T, r *multiRig, gap int, settle func([]ports.Scene) []ports.Scene) { | |||
| 78 | + | t.Helper() | |||
| 79 | + | rng := rand.New(rand.NewSource(20260928)) | |||
| 80 | + | var ids []ports.WindowID | |||
| 81 | + | next := ports.WindowID(1) | |||
| 82 | + | // A single keypress may publish a scene even when the action is | |||
| 83 | + | // inapplicable; check every resulting scene independently. | |||
| 84 | + | keys := []struct { | |||
| 85 | + | sym string | |||
| 86 | + | mods ports.Mods | |||
| 87 | + | }{ | |||
| 88 | + | {"Left", ports.ModAlt | ports.ModShift}, {"Right", ports.ModAlt | ports.ModShift}, | |||
| 89 | + | {"Left", ports.ModAlt}, {"Right", ports.ModAlt}, | |||
| 90 | + | {"Left", ports.ModAlt | ports.ModCtrl}, {"Right", ports.ModAlt | ports.ModCtrl}, | |||
| 91 | + | {"Left", ports.ModAlt | ports.ModCtrl | ports.ModShift}, {"Right", ports.ModAlt | ports.ModCtrl | ports.ModShift}, | |||
| 92 | + | {"bracketleft", ports.ModAlt}, {"bracketright", ports.ModAlt}, | |||
| 93 | + | {"f", ports.ModAlt}, {"f", ports.ModAlt | ports.ModShift}, | |||
| 94 | + | {"s", ports.ModAlt}, {"S", ports.ModAlt | ports.ModShift}, {"Up", ports.ModAlt}, {"Down", ports.ModAlt}, | |||
| 95 | + | } | |||
| 96 | + | const steps = 180 | |||
| 97 | + | for step := range steps { | |||
| 98 | + | var scenes []ports.Scene | |||
| 99 | + | switch { | |||
| 100 | + | case len(ids) < 2 || rng.Intn(5) == 0 && len(ids) < 12: | |||
| 101 | + | scenes = r.mapWindow(t, next) | |||
| 102 | + | ids = append(ids, next) | |||
| 103 | + | next++ | |||
| 104 | + | case rng.Intn(9) == 0: | |||
| 105 | + | i := rng.Intn(len(ids)) | |||
| 106 | + | r.client <- ports.WindowUnmapped{ID: ids[i]} | |||
| 107 | + | scenes = receive(t, r.scenes) | |||
| 108 | + | ids = append(ids[:i], ids[i+1:]...) | |||
| 109 | + | default: | |||
| 110 | + | k := keys[rng.Intn(len(keys))] | |||
| 111 | + | scenes = r.key(t, k.sym, k.mods) | |||
| 112 | + | } | |||
| 113 | + | if settle == nil { | |||
| 114 | + | checkBorderScenes(t, step, gap, scenes) | |||
| 115 | + | continue | |||
| 116 | + | } | |||
| 117 | + | checkBorderScenesMoving(t, step, scenes) | |||
| 118 | + | if rng.Intn(4) != 0 || step == steps-1 { | |||
| 119 | + | checkBorderScenes(t, step, gap, settle(scenes)) | |||
| 120 | + | } | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // sameRects reports whether two scene sets draw every window at the same | |||
| 125 | + | // rect. | |||
| 126 | + | func sameRects(a, b []ports.Scene) bool { | |||
| 127 | + | if len(a) != len(b) { | |||
| 128 | + | return false | |||
| 129 | + | } | |||
| 130 | + | for i := range a { | |||
| 131 | + | if !slices.EqualFunc(a[i].Windows, b[i].Windows, func(x, y ports.SceneWindow) bool { return x.ID == y.ID && x.Rect == y.Rect }) { | |||
| 132 | + | return false | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | return true | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | // checkBorderScenesMoving is what holds for a scene published while | |||
| 139 | + | // windows still move: every separator lies inside its output and belongs | |||
| 140 | + | // to a window that is shown, and at most one window is focused. | |||
| 141 | + | func checkBorderScenesMoving(t *testing.T, step int, scenes []ports.Scene) { | |||
| 142 | + | t.Helper() | |||
| 143 | + | focused := 0 | |||
| 144 | + | for _, s := range scenes { | |||
| 145 | + | o := ports.Rect{W: s.OutputWidth, H: s.OutputHeight} | |||
| 146 | + | for _, sep := range s.Separators { | |||
| 147 | + | r := sep.Rect | |||
| 148 | + | if r.W <= 0 || r.H <= 0 || r.X < 0 || r.Y < 0 || r.X+r.W > o.W || r.Y+r.H > o.H { | |||
| 149 | + | t.Fatalf("step %d %s (moving): line outside output: %+v; scene %+v", step, s.Output, sep, s) | |||
| 150 | + | } | |||
| 151 | + | if sep.Window != 0 && !slices.ContainsFunc(s.Windows, func(w ports.SceneWindow) bool { return w.ID == sep.Window && !w.Hidden }) { | |||
| 152 | + | t.Fatalf("step %d %s (moving): line belongs to a hidden or absent window: %+v; scene %+v", step, s.Output, sep, s) | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | for _, w := range s.Windows { | |||
| 156 | + | if w.Focused && !w.Hidden && !w.Popup { | |||
| 157 | + | focused++ | |||
| 158 | + | } | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | if focused > 1 { | |||
| 162 | + | t.Fatalf("step %d (moving): several focused windows: %+v", step, scenes) | |||
| 163 | + | } | |||
| 164 | + | } | |||
| 165 | + | ||||
| 63 | func checkBorderScenes(t *testing.T, step, gap int, scenes []ports.Scene) { | 166 | func checkBorderScenes(t *testing.T, step, gap int, scenes []ports.Scene) { | ||
| 64 | t.Helper() | 167 | t.Helper() | ||
| 65 | focused := 0 | 168 | focused := 0 | ||
M internal/core/core_test.go +66 -22
| @@ -383,15 +383,46 @@ func TestLayerKeyboardFocus(t *testing.T) { | |||
| 383 | 383 | } | |
| 384 | 384 | } | |
| 385 | 385 | ||
| 386 | - | func TestWorkspaceSwitch(t *testing.T) { | |
| 386 | + | func TestWorkspaceSwitch(t *testing.T) { workspaceSwitch(t, false) } | |
| 387 | + | ||
| 388 | + | // With animations on the scene right after the key shows the slide's start: | |
| 389 | + | // the test lands it before it asserts. | |
| 390 | + | func TestWorkspaceSwitchAnimated(t *testing.T) { workspaceSwitch(t, true) } | |
| 391 | + | ||
| 392 | + | // settledScene lands the springs an action started on the single output | |
| 393 | + | // OUT-1 of a rig with a stepClock and returns its scene; animations off: | |
| 394 | + | // the scene itself. | |
| 395 | + | func settledScene(t *testing.T, sc *stepClock, frames chan ports.OutputFrame, scenes chan []ports.Scene, s ports.Scene, mustAnimate bool) ports.Scene { | |
| 396 | + | t.Helper() | |
| 397 | + | if sc == nil { | |
| 398 | + | return s | |
| 399 | + | } | |
| 400 | + | set, ok := sc.settle(t, frames, scenes, "OUT-1") | |
| 401 | + | if !ok { | |
| 402 | + | if mustAnimate { | |
| 403 | + | t.Fatal("the action started no animation: the case checks nothing") | |
| 404 | + | } | |
| 405 | + | return s | |
| 406 | + | } | |
| 407 | + | return set[0] | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | func workspaceSwitch(t *testing.T, animated bool) { | |
| 387 | 411 | cfg := config.Defaults() | |
| 388 | - | cfg.Animations.On = false // the test reads the settled scene right after the key | |
| 412 | + | cfg.Animations.On = animated | |
| 389 | 413 | client := make(chan ports.ClientEvent, 8) | |
| 390 | 414 | input := make(chan ports.InputEvent, 8) | |
| 391 | 415 | output := make(chan ports.OutputEvent, 8) | |
| 392 | 416 | commands := make(chan ports.ClientCommand, 64) | |
| 393 | 417 | scenes := make(chan []ports.Scene, 1) | |
| 394 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 418 | + | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | |
| 419 | + | var sc *stepClock | |
| 420 | + | frames := make(chan ports.OutputFrame) | |
| 421 | + | if animated { | |
| 422 | + | sc = newStepClock(t) | |
| 423 | + | ch.Clock, ch.Frames = sc.clock, frames | |
| 424 | + | } | |
| 425 | + | c, err := core.New(cfg, ch) | |
| 395 | 426 | if err != nil { | |
| 396 | 427 | t.Fatal(err) | |
| 397 | 428 | } | |
| @@ -407,7 +438,7 @@ func TestWorkspaceSwitch(t *testing.T) { | |||
| 407 | 438 | } | |
| 408 | 439 | // AZERTY: the 2 key prints eacute; the default bind is on its physical key (code 3). | |
| 409 | 440 | input <- ports.KeyEvent{Keysym: "eacute", Base: "eacute", Keycode: 3, Mods: ports.ModSuper, Pressed: true} | |
| 410 | - | s := scene(t, scenes) | |
| 441 | + | s := settledScene(t, sc, frames, scenes, scene(t, scenes), animated) | |
| 411 | 442 | if len(s.Windows) != 1 || !s.Windows[0].Hidden { | |
| 412 | 443 | t.Fatal(s) | |
| 413 | 444 | } | |
| @@ -426,21 +457,32 @@ func TestWorkspaceSwitch(t *testing.T) { | |||
| 426 | 457 | } | |
| 427 | 458 | // A new window opens on workspace 2, alone and borderless. | |
| 428 | 459 | client <- ports.WindowMapped{ID: 2} | |
| 429 | - | s = scene(t, scenes) | |
| 460 | + | s = settledScene(t, sc, frames, scenes, scene(t, scenes), false) | |
| 430 | 461 | if len(s.Windows) != 2 || s.Windows[1].ID != 2 || s.Windows[1].Hidden || s.Windows[1].Inset != 0 { | |
| 431 | 462 | t.Fatal(s) | |
| 432 | 463 | } | |
| 433 | 464 | } | |
| 434 | 465 | ||
| 435 | - | func TestClickAfterWorkspaceSwitch(t *testing.T) { | |
| 466 | + | func TestClickAfterWorkspaceSwitch(t *testing.T) { clickAfterWorkspaceSwitch(t, false) } | |
| 467 | + | ||
| 468 | + | func TestClickAfterWorkspaceSwitchAnimated(t *testing.T) { clickAfterWorkspaceSwitch(t, true) } | |
| 469 | + | ||
| 470 | + | func clickAfterWorkspaceSwitch(t *testing.T, animated bool) { | |
| 436 | 471 | cfg := config.Defaults() | |
| 437 | - | cfg.Animations.On = false // the test reads the settled scene right after the key | |
| 472 | + | cfg.Animations.On = animated | |
| 438 | 473 | client := make(chan ports.ClientEvent, 8) | |
| 439 | 474 | input := make(chan ports.InputEvent, 8) | |
| 440 | 475 | output := make(chan ports.OutputEvent, 8) | |
| 441 | 476 | commands := make(chan ports.ClientCommand, 64) | |
| 442 | 477 | scenes := make(chan []ports.Scene, 1) | |
| 443 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | |
| 478 | + | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} | |
| 479 | + | var sc *stepClock | |
| 480 | + | frames := make(chan ports.OutputFrame) | |
| 481 | + | if animated { | |
| 482 | + | sc = newStepClock(t) | |
| 483 | + | ch.Clock, ch.Frames = sc.clock, frames | |
| 484 | + | } | |
| 485 | + | c, err := core.New(cfg, ch) | |
| 444 | 486 | if err != nil { | |
| 445 | 487 | t.Fatal(err) | |
| 446 | 488 | } | |
| @@ -460,7 +502,7 @@ func TestClickAfterWorkspaceSwitch(t *testing.T) { | |||
| 460 | 502 | } | |
| 461 | 503 | // Cmd+2 hides window 1; the pointer must leave it. | |
| 462 | 504 | input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true} | |
| 463 | - | scene(t, scenes) | |
| 505 | + | settledScene(t, sc, frames, scenes, scene(t, scenes), animated) | |
| 464 | 506 | var cleared bool | |
| 465 | 507 | for len(commands) > 0 { | |
| 466 | 508 | if v, ok := (<-commands).(ports.PointerFocus); ok && v.ID == 0 { | |
| @@ -474,7 +516,7 @@ func TestClickAfterWorkspaceSwitch(t *testing.T) { | |||
| 474 | 516 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | |
| 475 | 517 | input <- ports.PointerButton{Button: 0x110} | |
| 476 | 518 | client <- ports.WindowMapped{ID: 2} | |
| 477 | - | s := scene(t, scenes) | |
| 519 | + | s := settledScene(t, sc, frames, scenes, scene(t, scenes), false) | |
| 478 | 520 | if s.Windows[0].ID != 1 || !s.Windows[0].Hidden || s.Windows[1].Hidden { | |
| 479 | 521 | t.Fatal(s) | |
| 480 | 522 | } | |
| @@ -1024,18 +1066,20 @@ func TestExternalFullscreenInOverview(t *testing.T) { | |||
| 1024 | 1066 | // focus-window-down from a fullscreen tile at the bottom of its column | |
| 1025 | 1067 | // goes to the next workspace and fullscreen stays. | |
| 1026 | 1068 | func TestFocusWindowDownKeepsFullscreen(t *testing.T) { | |
| 1027 | - | r := startMulti(t, func(c *ports.Config) { c.Layout.Overflow = "fixed"; c.Animations.On = false }, left) | |
| 1028 | - | r.mapWindow(t, 1) | |
| 1029 | - | r.mapWindow(t, 2) | |
| 1030 | - | r.key(t, "f", ports.ModAlt|ports.ModShift) | |
| 1031 | - | set := r.key(t, "Down", ports.ModAlt) | |
| 1032 | - | if got, _ := windowsOf(set, "DP-1"); len(got) != 0 { | |
| 1033 | - | t.Fatal("still on the first workspace:", got) | |
| 1034 | - | } | |
| 1035 | - | set = r.key(t, "Up", ports.ModAlt) | |
| 1036 | - | if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{2}) || focused != 2 { | |
| 1037 | - | t.Fatal("fullscreen lost:", got, focused) | |
| 1038 | - | } | |
| 1069 | + | both(t, func(t *testing.T, animated bool) { | |
| 1070 | + | r := startLanding(t, animated, func(c *ports.Config) { c.Layout.Overflow = "fixed" }, left) | |
| 1071 | + | r.mapLanded(t, 1) | |
| 1072 | + | r.mapLanded(t, 2) | |
| 1073 | + | r.keyLanded(t, "f", ports.ModAlt|ports.ModShift) | |
| 1074 | + | set := r.keyLanded(t, "Down", ports.ModAlt) | |
| 1075 | + | if got, _ := windowsOf(set, "DP-1"); len(got) != 0 { | |
| 1076 | + | t.Fatal("still on the first workspace:", got) | |
| 1077 | + | } | |
| 1078 | + | set = r.keyLanded(t, "Up", ports.ModAlt) | |
| 1079 | + | if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{2}) || focused != 2 { | |
| 1080 | + | t.Fatal("fullscreen lost:", got, focused) | |
| 1081 | + | } | |
| 1082 | + | }) | |
| 1039 | 1083 | } | |
| 1040 | 1084 | ||
| 1041 | 1085 | // A refused fullscreen request still gets a configure, unchanged: clients | |
| @@ -383,15 +383,46 @@ func TestLayerKeyboardFocus(t *testing.T) { | |||||
| 383 | } | 383 | } | ||
| 384 | } | 384 | } | ||
| 385 | 385 | ||||
| 386 | - | func TestWorkspaceSwitch(t *testing.T) { | 386 | + | func TestWorkspaceSwitch(t *testing.T) { workspaceSwitch(t, false) } |
| 387 | + | ||||
| 388 | + | // With animations on the scene right after the key shows the slide's start: | |||
| 389 | + | // the test lands it before it asserts. | |||
| 390 | + | func TestWorkspaceSwitchAnimated(t *testing.T) { workspaceSwitch(t, true) } | |||
| 391 | + | ||||
| 392 | + | // settledScene lands the springs an action started on the single output | |||
| 393 | + | // OUT-1 of a rig with a stepClock and returns its scene; animations off: | |||
| 394 | + | // the scene itself. | |||
| 395 | + | func settledScene(t *testing.T, sc *stepClock, frames chan ports.OutputFrame, scenes chan []ports.Scene, s ports.Scene, mustAnimate bool) ports.Scene { | |||
| 396 | + | t.Helper() | |||
| 397 | + | if sc == nil { | |||
| 398 | + | return s | |||
| 399 | + | } | |||
| 400 | + | set, ok := sc.settle(t, frames, scenes, "OUT-1") | |||
| 401 | + | if !ok { | |||
| 402 | + | if mustAnimate { | |||
| 403 | + | t.Fatal("the action started no animation: the case checks nothing") | |||
| 404 | + | } | |||
| 405 | + | return s | |||
| 406 | + | } | |||
| 407 | + | return set[0] | |||
| 408 | + | } | |||
| 409 | + | ||||
| 410 | + | func workspaceSwitch(t *testing.T, animated bool) { | |||
| 387 | cfg := config.Defaults() | 411 | cfg := config.Defaults() | ||
| 388 | - | cfg.Animations.On = false // the test reads the settled scene right after the key | 412 | + | cfg.Animations.On = animated |
| 389 | client := make(chan ports.ClientEvent, 8) | 413 | client := make(chan ports.ClientEvent, 8) | ||
| 390 | input := make(chan ports.InputEvent, 8) | 414 | input := make(chan ports.InputEvent, 8) | ||
| 391 | output := make(chan ports.OutputEvent, 8) | 415 | output := make(chan ports.OutputEvent, 8) | ||
| 392 | commands := make(chan ports.ClientCommand, 64) | 416 | commands := make(chan ports.ClientCommand, 64) | ||
| 393 | scenes := make(chan []ports.Scene, 1) | 417 | scenes := make(chan []ports.Scene, 1) | ||
| 394 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 418 | + | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} |
| 419 | + | var sc *stepClock | |||
| 420 | + | frames := make(chan ports.OutputFrame) | |||
| 421 | + | if animated { | |||
| 422 | + | sc = newStepClock(t) | |||
| 423 | + | ch.Clock, ch.Frames = sc.clock, frames | |||
| 424 | + | } | |||
| 425 | + | c, err := core.New(cfg, ch) | |||
| 395 | if err != nil { | 426 | if err != nil { | ||
| 396 | t.Fatal(err) | 427 | t.Fatal(err) | ||
| 397 | } | 428 | } | ||
| @@ -407,7 +438,7 @@ func TestWorkspaceSwitch(t *testing.T) { | |||||
| 407 | } | 438 | } | ||
| 408 | // AZERTY: the 2 key prints eacute; the default bind is on its physical key (code 3). | 439 | // AZERTY: the 2 key prints eacute; the default bind is on its physical key (code 3). | ||
| 409 | input <- ports.KeyEvent{Keysym: "eacute", Base: "eacute", Keycode: 3, Mods: ports.ModSuper, Pressed: true} | 440 | input <- ports.KeyEvent{Keysym: "eacute", Base: "eacute", Keycode: 3, Mods: ports.ModSuper, Pressed: true} | ||
| 410 | - | s := scene(t, scenes) | 441 | + | s := settledScene(t, sc, frames, scenes, scene(t, scenes), animated) |
| 411 | if len(s.Windows) != 1 || !s.Windows[0].Hidden { | 442 | if len(s.Windows) != 1 || !s.Windows[0].Hidden { | ||
| 412 | t.Fatal(s) | 443 | t.Fatal(s) | ||
| 413 | } | 444 | } | ||
| @@ -426,21 +457,32 @@ func TestWorkspaceSwitch(t *testing.T) { | |||||
| 426 | } | 457 | } | ||
| 427 | // A new window opens on workspace 2, alone and borderless. | 458 | // A new window opens on workspace 2, alone and borderless. | ||
| 428 | client <- ports.WindowMapped{ID: 2} | 459 | client <- ports.WindowMapped{ID: 2} | ||
| 429 | - | s = scene(t, scenes) | 460 | + | s = settledScene(t, sc, frames, scenes, scene(t, scenes), false) |
| 430 | if len(s.Windows) != 2 || s.Windows[1].ID != 2 || s.Windows[1].Hidden || s.Windows[1].Inset != 0 { | 461 | if len(s.Windows) != 2 || s.Windows[1].ID != 2 || s.Windows[1].Hidden || s.Windows[1].Inset != 0 { | ||
| 431 | t.Fatal(s) | 462 | t.Fatal(s) | ||
| 432 | } | 463 | } | ||
| 433 | } | 464 | } | ||
| 434 | 465 | ||||
| 435 | - | func TestClickAfterWorkspaceSwitch(t *testing.T) { | 466 | + | func TestClickAfterWorkspaceSwitch(t *testing.T) { clickAfterWorkspaceSwitch(t, false) } |
| 467 | + | ||||
| 468 | + | func TestClickAfterWorkspaceSwitchAnimated(t *testing.T) { clickAfterWorkspaceSwitch(t, true) } | |||
| 469 | + | ||||
| 470 | + | func clickAfterWorkspaceSwitch(t *testing.T, animated bool) { | |||
| 436 | cfg := config.Defaults() | 471 | cfg := config.Defaults() | ||
| 437 | - | cfg.Animations.On = false // the test reads the settled scene right after the key | 472 | + | cfg.Animations.On = animated |
| 438 | client := make(chan ports.ClientEvent, 8) | 473 | client := make(chan ports.ClientEvent, 8) | ||
| 439 | input := make(chan ports.InputEvent, 8) | 474 | input := make(chan ports.InputEvent, 8) | ||
| 440 | output := make(chan ports.OutputEvent, 8) | 475 | output := make(chan ports.OutputEvent, 8) | ||
| 441 | commands := make(chan ports.ClientCommand, 64) | 476 | commands := make(chan ports.ClientCommand, 64) | ||
| 442 | scenes := make(chan []ports.Scene, 1) | 477 | scenes := make(chan []ports.Scene, 1) | ||
| 443 | - | c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes}) | 478 | + | ch := core.Channels{Client: client, Input: input, Output: output, Commands: commands, Scenes: scenes} |
| 479 | + | var sc *stepClock | |||
| 480 | + | frames := make(chan ports.OutputFrame) | |||
| 481 | + | if animated { | |||
| 482 | + | sc = newStepClock(t) | |||
| 483 | + | ch.Clock, ch.Frames = sc.clock, frames | |||
| 484 | + | } | |||
| 485 | + | c, err := core.New(cfg, ch) | |||
| 444 | if err != nil { | 486 | if err != nil { | ||
| 445 | t.Fatal(err) | 487 | t.Fatal(err) | ||
| 446 | } | 488 | } | ||
| @@ -460,7 +502,7 @@ func TestClickAfterWorkspaceSwitch(t *testing.T) { | |||||
| 460 | } | 502 | } | ||
| 461 | // Cmd+2 hides window 1; the pointer must leave it. | 503 | // Cmd+2 hides window 1; the pointer must leave it. | ||
| 462 | input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true} | 504 | input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModSuper, Pressed: true} | ||
| 463 | - | scene(t, scenes) | 505 | + | settledScene(t, sc, frames, scenes, scene(t, scenes), animated) |
| 464 | var cleared bool | 506 | var cleared bool | ||
| 465 | for len(commands) > 0 { | 507 | for len(commands) > 0 { | ||
| 466 | if v, ok := (<-commands).(ports.PointerFocus); ok && v.ID == 0 { | 508 | if v, ok := (<-commands).(ports.PointerFocus); ok && v.ID == 0 { | ||
| @@ -474,7 +516,7 @@ func TestClickAfterWorkspaceSwitch(t *testing.T) { | |||||
| 474 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | 516 | input <- ports.PointerButton{Button: 0x110, Pressed: true} | ||
| 475 | input <- ports.PointerButton{Button: 0x110} | 517 | input <- ports.PointerButton{Button: 0x110} | ||
| 476 | client <- ports.WindowMapped{ID: 2} | 518 | client <- ports.WindowMapped{ID: 2} | ||
| 477 | - | s := scene(t, scenes) | 519 | + | s := settledScene(t, sc, frames, scenes, scene(t, scenes), false) |
| 478 | if s.Windows[0].ID != 1 || !s.Windows[0].Hidden || s.Windows[1].Hidden { | 520 | if s.Windows[0].ID != 1 || !s.Windows[0].Hidden || s.Windows[1].Hidden { | ||
| 479 | t.Fatal(s) | 521 | t.Fatal(s) | ||
| 480 | } | 522 | } | ||
| @@ -1024,18 +1066,20 @@ func TestExternalFullscreenInOverview(t *testing.T) { | |||||
| 1024 | // focus-window-down from a fullscreen tile at the bottom of its column | 1066 | // focus-window-down from a fullscreen tile at the bottom of its column | ||
| 1025 | // goes to the next workspace and fullscreen stays. | 1067 | // goes to the next workspace and fullscreen stays. | ||
| 1026 | func TestFocusWindowDownKeepsFullscreen(t *testing.T) { | 1068 | func TestFocusWindowDownKeepsFullscreen(t *testing.T) { | ||
| 1027 | - | r := startMulti(t, func(c *ports.Config) { c.Layout.Overflow = "fixed"; c.Animations.On = false }, left) | 1069 | + | both(t, func(t *testing.T, animated bool) { |
| 1028 | - | r.mapWindow(t, 1) | 1070 | + | r := startLanding(t, animated, func(c *ports.Config) { c.Layout.Overflow = "fixed" }, left) |
| 1029 | - | r.mapWindow(t, 2) | 1071 | + | r.mapLanded(t, 1) |
| 1030 | - | r.key(t, "f", ports.ModAlt|ports.ModShift) | 1072 | + | r.mapLanded(t, 2) |
| 1031 | - | set := r.key(t, "Down", ports.ModAlt) | 1073 | + | r.keyLanded(t, "f", ports.ModAlt|ports.ModShift) |
| 1032 | - | if got, _ := windowsOf(set, "DP-1"); len(got) != 0 { | 1074 | + | set := r.keyLanded(t, "Down", ports.ModAlt) |
| 1033 | - | t.Fatal("still on the first workspace:", got) | 1075 | + | if got, _ := windowsOf(set, "DP-1"); len(got) != 0 { |
| 1034 | - | } | 1076 | + | t.Fatal("still on the first workspace:", got) |
| 1035 | - | set = r.key(t, "Up", ports.ModAlt) | 1077 | + | } |
| 1036 | - | if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{2}) || focused != 2 { | 1078 | + | set = r.keyLanded(t, "Up", ports.ModAlt) |
| 1037 | - | t.Fatal("fullscreen lost:", got, focused) | 1079 | + | if got, focused := windowsOf(set, "DP-1"); !reflect.DeepEqual(got, []ports.WindowID{2}) || focused != 2 { |
| 1038 | - | } | 1080 | + | t.Fatal("fullscreen lost:", got, focused) |
| 1081 | + | } | |||
| 1082 | + | }) | |||
| 1039 | } | 1083 | } | ||
| 1040 | 1084 | ||||
| 1041 | // A refused fullscreen request still gets a configure, unchanged: clients | 1085 | // A refused fullscreen request still gets a configure, unchanged: clients | ||
M internal/core/drag_test.go +71 -5
| @@ -21,11 +21,35 @@ type dragRig struct { | |||
| 21 | 21 | scenes chan []ports.Scene | |
| 22 | 22 | last []ports.Scene | |
| 23 | 23 | epoch *atomic.Value | |
| 24 | + | // With a clock the test moves (animated rigs), frames carries the page | |
| 25 | + | // flips and outputs lists the plugged ones: settle lands every spring | |
| 26 | + | // before it reads the scene. | |
| 27 | + | frames chan ports.OutputFrame | |
| 28 | + | clock *stepClock | |
| 29 | + | outputs []string | |
| 24 | 30 | } | |
| 25 | 31 | ||
| 26 | 32 | // startDrag runs core with Super as Cmd, no border, 10 px gaps, and | |
| 27 | 33 | // the given outputs. edit may change the config. | |
| 28 | 34 | func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | |
| 35 | + | t.Helper() | |
| 36 | + | return newDragRig(t, false, edit, outs...) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | // startAnimatedDragRig is startDragRig with animations on and a clock the | |
| 40 | + | // test moves: settle moves it past every spring and sends the page flips, | |
| 41 | + | // so the scene it returns is the settled one, deterministically. | |
| 42 | + | func startAnimatedDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | |
| 43 | + | t.Helper() | |
| 44 | + | return newDragRig(t, true, func(c *ports.Config) { | |
| 45 | + | c.Animations.On = true | |
| 46 | + | if edit != nil { | |
| 47 | + | edit(c) | |
| 48 | + | } | |
| 49 | + | }, outs...) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | func newDragRig(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | |
| 29 | 53 | t.Helper() | |
| 30 | 54 | cfg := config.Defaults() | |
| 31 | 55 | cfg.Border.Width = 0 | |
| @@ -45,7 +69,12 @@ func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||
| 45 | 69 | output: make(chan ports.OutputEvent), commands: make(chan ports.ClientCommand, 4096), | |
| 46 | 70 | scenes: make(chan []ports.Scene, 1), epoch: epoch, | |
| 47 | 71 | } | |
| 48 | - | c, err := core.New(cfg, core.Channels{Security: gate, Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes}) | |
| 72 | + | ch := core.Channels{Security: gate, Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes} | |
| 73 | + | if animated { | |
| 74 | + | r.frames, r.clock = make(chan ports.OutputFrame), newStepClock(t) | |
| 75 | + | ch.Frames, ch.Clock = r.frames, r.clock.clock | |
| 76 | + | } | |
| 77 | + | c, err := core.New(cfg, ch) | |
| 49 | 78 | if err != nil { | |
| 50 | 79 | t.Fatal(err) | |
| 51 | 80 | } | |
| @@ -55,6 +84,7 @@ func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||
| 55 | 84 | t.Cleanup(func() { cancel(); <-done }) | |
| 56 | 85 | for _, o := range outs { | |
| 57 | 86 | r.output <- ports.OutputAdded{Info: o} | |
| 87 | + | r.outputs = append(r.outputs, o.Name) | |
| 58 | 88 | r.settle(t) | |
| 59 | 89 | } | |
| 60 | 90 | return r | |
| @@ -79,9 +109,17 @@ func send[T any](ch chan T, v T) { | |||
| 79 | 109 | const barrier ports.WindowID = 1 << 30 | |
| 80 | 110 | ||
| 81 | 111 | // settle returns the scene set once core handled every event sent so | |
| 82 | - | // far. The second barrier is taken only after the first one published. | |
| 112 | + | // far. The second barrier is taken only after the first one published. An | |
| 113 | + | // animated rig first moves its clock past every spring and flips each | |
| 114 | + | // output, so the set is the settled one. | |
| 83 | 115 | func (r *dragRig) settle(t *testing.T) []ports.Scene { | |
| 84 | 116 | t.Helper() | |
| 117 | + | if r.frames != nil { | |
| 118 | + | r.clock.advance(5 * time.Second) | |
| 119 | + | for _, o := range r.outputs { | |
| 120 | + | send(r.frames, ports.OutputFrame{Output: o}) | |
| 121 | + | } | |
| 122 | + | } | |
| 85 | 123 | send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier})) | |
| 86 | 124 | send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier})) | |
| 87 | 125 | select { | |
| @@ -332,8 +370,28 @@ func TestDragTileZonesAndHints(t *testing.T) { | |||
| 332 | 370 | } | |
| 333 | 371 | } | |
| 334 | 372 | ||
| 373 | + | // startDragMode is the drag rig with animations off, or on and landed by | |
| 374 | + | // settle. | |
| 375 | + | func startDragMode(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | |
| 376 | + | t.Helper() | |
| 377 | + | if animated { | |
| 378 | + | return startAnimatedDragRig(t, edit, outs...) | |
| 379 | + | } | |
| 380 | + | return startDragRig(t, func(c *ports.Config) { | |
| 381 | + | if edit != nil { | |
| 382 | + | edit(c) | |
| 383 | + | } | |
| 384 | + | c.Animations.On = false | |
| 385 | + | }, outs...) | |
| 386 | + | } | |
| 387 | + | ||
| 335 | 388 | func TestDragTileGapInsert(t *testing.T) { | |
| 336 | - | r := startDragRig(t, func(c *ports.Config) { c.Animations.On = false; c.Layout.MaxColumns = 3 }, dragOut) | |
| 389 | + | both(t, func(t *testing.T, animated bool) { | |
| 390 | + | dragTileGapInsert(t, startDragMode(t, animated, func(c *ports.Config) { c.Layout.MaxColumns = 3 }, dragOut)) | |
| 391 | + | }) | |
| 392 | + | } | |
| 393 | + | ||
| 394 | + | func dragTileGapInsert(t *testing.T, r *dragRig) { | |
| 337 | 395 | for id := ports.WindowID(1); id <= 3; id++ { | |
| 338 | 396 | r.client <- ports.WindowMapped{ID: id} | |
| 339 | 397 | r.settle(t) | |
| @@ -484,7 +542,10 @@ func TestLockClearsDrag(t *testing.T) { | |||
| 484 | 542 | } | |
| 485 | 543 | ||
| 486 | 544 | func TestDragEdgeScroll(t *testing.T) { | |
| 487 | - | r := startDragRig(t, animationsOff, dragOut) | |
| 545 | + | both(t, func(t *testing.T, animated bool) { dragEdgeScroll(t, startDragMode(t, animated, nil, dragOut)) }) | |
| 546 | + | } | |
| 547 | + | ||
| 548 | + | func dragEdgeScroll(t *testing.T, r *dragRig) { | |
| 488 | 549 | for id := ports.WindowID(1); id <= 4; id++ { | |
| 489 | 550 | r.client <- ports.WindowMapped{ID: id} | |
| 490 | 551 | r.settle(t) | |
| @@ -623,7 +684,12 @@ func TestClientDragUsesLastPress(t *testing.T) { | |||
| 623 | 684 | } | |
| 624 | 685 | ||
| 625 | 686 | func TestEdgeScrollPublishesWithoutTarget(t *testing.T) { | |
| 626 | - | r := startDragRig(t, animationsOff, dragOut) | |
| 687 | + | both(t, func(t *testing.T, animated bool) { | |
| 688 | + | edgeScrollPublishesWithoutTarget(t, startDragMode(t, animated, nil, dragOut)) | |
| 689 | + | }) | |
| 690 | + | } | |
| 691 | + | ||
| 692 | + | func edgeScrollPublishesWithoutTarget(t *testing.T, r *dragRig) { | |
| 627 | 693 | for id := ports.WindowID(1); id <= 4; id++ { | |
| 628 | 694 | r.client <- ports.WindowMapped{ID: id} | |
| 629 | 695 | r.settle(t) | |
| @@ -21,11 +21,35 @@ type dragRig struct { | |||||
| 21 | scenes chan []ports.Scene | 21 | scenes chan []ports.Scene | ||
| 22 | last []ports.Scene | 22 | last []ports.Scene | ||
| 23 | epoch *atomic.Value | 23 | epoch *atomic.Value | ||
| 24 | + | // With a clock the test moves (animated rigs), frames carries the page | |||
| 25 | + | // flips and outputs lists the plugged ones: settle lands every spring | |||
| 26 | + | // before it reads the scene. | |||
| 27 | + | frames chan ports.OutputFrame | |||
| 28 | + | clock *stepClock | |||
| 29 | + | outputs []string | |||
| 24 | } | 30 | } | ||
| 25 | 31 | ||||
| 26 | // startDrag runs core with Super as Cmd, no border, 10 px gaps, and | 32 | // startDrag runs core with Super as Cmd, no border, 10 px gaps, and | ||
| 27 | // the given outputs. edit may change the config. | 33 | // the given outputs. edit may change the config. | ||
| 28 | func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | 34 | func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | ||
| 35 | + | t.Helper() | |||
| 36 | + | return newDragRig(t, false, edit, outs...) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | // startAnimatedDragRig is startDragRig with animations on and a clock the | |||
| 40 | + | // test moves: settle moves it past every spring and sends the page flips, | |||
| 41 | + | // so the scene it returns is the settled one, deterministically. | |||
| 42 | + | func startAnimatedDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | |||
| 43 | + | t.Helper() | |||
| 44 | + | return newDragRig(t, true, func(c *ports.Config) { | |||
| 45 | + | c.Animations.On = true | |||
| 46 | + | if edit != nil { | |||
| 47 | + | edit(c) | |||
| 48 | + | } | |||
| 49 | + | }, outs...) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | func newDragRig(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | |||
| 29 | t.Helper() | 53 | t.Helper() | ||
| 30 | cfg := config.Defaults() | 54 | cfg := config.Defaults() | ||
| 31 | cfg.Border.Width = 0 | 55 | cfg.Border.Width = 0 | ||
| @@ -45,7 +69,12 @@ func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||||
| 45 | output: make(chan ports.OutputEvent), commands: make(chan ports.ClientCommand, 4096), | 69 | output: make(chan ports.OutputEvent), commands: make(chan ports.ClientCommand, 4096), | ||
| 46 | scenes: make(chan []ports.Scene, 1), epoch: epoch, | 70 | scenes: make(chan []ports.Scene, 1), epoch: epoch, | ||
| 47 | } | 71 | } | ||
| 48 | - | c, err := core.New(cfg, core.Channels{Security: gate, Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes}) | 72 | + | ch := core.Channels{Security: gate, Client: r.client, Input: r.input, Output: r.output, Commands: r.commands, Scenes: r.scenes} |
| 73 | + | if animated { | |||
| 74 | + | r.frames, r.clock = make(chan ports.OutputFrame), newStepClock(t) | |||
| 75 | + | ch.Frames, ch.Clock = r.frames, r.clock.clock | |||
| 76 | + | } | |||
| 77 | + | c, err := core.New(cfg, ch) | |||
| 49 | if err != nil { | 78 | if err != nil { | ||
| 50 | t.Fatal(err) | 79 | t.Fatal(err) | ||
| 51 | } | 80 | } | ||
| @@ -55,6 +84,7 @@ func startDragRig(t *testing.T, edit func(*ports.Config), outs ...ports.OutputIn | |||||
| 55 | t.Cleanup(func() { cancel(); <-done }) | 84 | t.Cleanup(func() { cancel(); <-done }) | ||
| 56 | for _, o := range outs { | 85 | for _, o := range outs { | ||
| 57 | r.output <- ports.OutputAdded{Info: o} | 86 | r.output <- ports.OutputAdded{Info: o} | ||
| 87 | + | r.outputs = append(r.outputs, o.Name) | |||
| 58 | r.settle(t) | 88 | r.settle(t) | ||
| 59 | } | 89 | } | ||
| 60 | return r | 90 | return r | ||
| @@ -79,9 +109,17 @@ func send[T any](ch chan T, v T) { | |||||
| 79 | const barrier ports.WindowID = 1 << 30 | 109 | const barrier ports.WindowID = 1 << 30 | ||
| 80 | 110 | ||||
| 81 | // settle returns the scene set once core handled every event sent so | 111 | // settle returns the scene set once core handled every event sent so | ||
| 82 | - | // far. The second barrier is taken only after the first one published. | 112 | + | // far. The second barrier is taken only after the first one published. An |
| 113 | + | // animated rig first moves its clock past every spring and flips each | |||
| 114 | + | // output, so the set is the settled one. | |||
| 83 | func (r *dragRig) settle(t *testing.T) []ports.Scene { | 115 | func (r *dragRig) settle(t *testing.T) []ports.Scene { | ||
| 84 | t.Helper() | 116 | t.Helper() | ||
| 117 | + | if r.frames != nil { | |||
| 118 | + | r.clock.advance(5 * time.Second) | |||
| 119 | + | for _, o := range r.outputs { | |||
| 120 | + | send(r.frames, ports.OutputFrame{Output: o}) | |||
| 121 | + | } | |||
| 122 | + | } | |||
| 85 | send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier})) | 123 | send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier})) | ||
| 86 | send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier})) | 124 | send(r.client, ports.ClientEvent(ports.WindowResized{ID: barrier})) | ||
| 87 | select { | 125 | select { | ||
| @@ -332,8 +370,28 @@ func TestDragTileZonesAndHints(t *testing.T) { | |||||
| 332 | } | 370 | } | ||
| 333 | } | 371 | } | ||
| 334 | 372 | ||||
| 373 | + | // startDragMode is the drag rig with animations off, or on and landed by | |||
| 374 | + | // settle. | |||
| 375 | + | func startDragMode(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *dragRig { | |||
| 376 | + | t.Helper() | |||
| 377 | + | if animated { | |||
| 378 | + | return startAnimatedDragRig(t, edit, outs...) | |||
| 379 | + | } | |||
| 380 | + | return startDragRig(t, func(c *ports.Config) { | |||
| 381 | + | if edit != nil { | |||
| 382 | + | edit(c) | |||
| 383 | + | } | |||
| 384 | + | c.Animations.On = false | |||
| 385 | + | }, outs...) | |||
| 386 | + | } | |||
| 387 | + | ||||
| 335 | func TestDragTileGapInsert(t *testing.T) { | 388 | func TestDragTileGapInsert(t *testing.T) { | ||
| 336 | - | r := startDragRig(t, func(c *ports.Config) { c.Animations.On = false; c.Layout.MaxColumns = 3 }, dragOut) | 389 | + | both(t, func(t *testing.T, animated bool) { |
| 390 | + | dragTileGapInsert(t, startDragMode(t, animated, func(c *ports.Config) { c.Layout.MaxColumns = 3 }, dragOut)) | |||
| 391 | + | }) | |||
| 392 | + | } | |||
| 393 | + | ||||
| 394 | + | func dragTileGapInsert(t *testing.T, r *dragRig) { | |||
| 337 | for id := ports.WindowID(1); id <= 3; id++ { | 395 | for id := ports.WindowID(1); id <= 3; id++ { | ||
| 338 | r.client <- ports.WindowMapped{ID: id} | 396 | r.client <- ports.WindowMapped{ID: id} | ||
| 339 | r.settle(t) | 397 | r.settle(t) | ||
| @@ -484,7 +542,10 @@ func TestLockClearsDrag(t *testing.T) { | |||||
| 484 | } | 542 | } | ||
| 485 | 543 | ||||
| 486 | func TestDragEdgeScroll(t *testing.T) { | 544 | func TestDragEdgeScroll(t *testing.T) { | ||
| 487 | - | r := startDragRig(t, animationsOff, dragOut) | 545 | + | both(t, func(t *testing.T, animated bool) { dragEdgeScroll(t, startDragMode(t, animated, nil, dragOut)) }) |
| 546 | + | } | |||
| 547 | + | ||||
| 548 | + | func dragEdgeScroll(t *testing.T, r *dragRig) { | |||
| 488 | for id := ports.WindowID(1); id <= 4; id++ { | 549 | for id := ports.WindowID(1); id <= 4; id++ { | ||
| 489 | r.client <- ports.WindowMapped{ID: id} | 550 | r.client <- ports.WindowMapped{ID: id} | ||
| 490 | r.settle(t) | 551 | r.settle(t) | ||
| @@ -623,7 +684,12 @@ func TestClientDragUsesLastPress(t *testing.T) { | |||||
| 623 | } | 684 | } | ||
| 624 | 685 | ||||
| 625 | func TestEdgeScrollPublishesWithoutTarget(t *testing.T) { | 686 | func TestEdgeScrollPublishesWithoutTarget(t *testing.T) { | ||
| 626 | - | r := startDragRig(t, animationsOff, dragOut) | 687 | + | both(t, func(t *testing.T, animated bool) { |
| 688 | + | edgeScrollPublishesWithoutTarget(t, startDragMode(t, animated, nil, dragOut)) | |||
| 689 | + | }) | |||
| 690 | + | } | |||
| 691 | + | ||||
| 692 | + | func edgeScrollPublishesWithoutTarget(t *testing.T, r *dragRig) { | |||
| 627 | for id := ports.WindowID(1); id <= 4; id++ { | 693 | for id := ports.WindowID(1); id <= 4; id++ { | ||
| 628 | r.client <- ports.WindowMapped{ID: id} | 694 | r.client <- ports.WindowMapped{ID: id} | ||
| 629 | r.settle(t) | 695 | r.settle(t) | ||
M internal/core/free_core_test.go +21 -19
| @@ -21,23 +21,25 @@ func windowIn(set []ports.Scene, output string, id ports.WindowID) (ports.SceneW | |||
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | 23 | func TestFreeFloatKeepsPlaceAcrossMonitors(t *testing.T) { | |
| 24 | - | r := startMulti(t, func(c *ports.Config) { c.Animations.On = false; c.Layout.Gaps = 0 }, left, right) | |
| 25 | - | r.mapWindow(t, 1) | |
| 26 | - | r.mapWindow(t, 2) | |
| 27 | - | r.key(t, "space", ports.ModAlt|ports.ModShift) | |
| 28 | - | // Nudge left twice: 100 px left of centre on a 200 px output stops at | |
| 29 | - | // the edge. | |
| 30 | - | r.key(t, "Left", ports.ModAlt|ports.ModShift) | |
| 31 | - | set := r.key(t, "Left", ports.ModAlt|ports.ModShift) | |
| 32 | - | before, ok := windowIn(set, "DP-1", 2) | |
| 33 | - | if !ok || !before.Floating || before.Rect.X != 0 { | |
| 34 | - | t.Fatalf("%+v", before) | |
| 35 | - | } | |
| 36 | - | // The workspace moves to the 400x200 output: the centre keeps its | |
| 37 | - | // fractions of the usable area (1/4, 1/2), the size stays. | |
| 38 | - | set = r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |
| 39 | - | after, ok := windowIn(set, "DP-2", 2) | |
| 40 | - | if !ok || after.Rect.W != before.Rect.W || after.Rect.X+after.Rect.W/2 != 100 || after.Rect.Y+after.Rect.H/2 != 100 { | |
| 41 | - | t.Fatalf("%+v", after) | |
| 42 | - | } | |
| 24 | + | both(t, func(t *testing.T, animated bool) { | |
| 25 | + | r := startLanding(t, animated, func(c *ports.Config) { c.Layout.Gaps = 0 }, left, right) | |
| 26 | + | r.mapLanded(t, 1) | |
| 27 | + | r.mapLanded(t, 2) | |
| 28 | + | r.keyLanded(t, "space", ports.ModAlt|ports.ModShift) | |
| 29 | + | // Nudge left twice: 100 px left of centre on a 200 px output stops at | |
| 30 | + | // the edge. | |
| 31 | + | r.keyLanded(t, "Left", ports.ModAlt|ports.ModShift) | |
| 32 | + | set := r.keyLanded(t, "Left", ports.ModAlt|ports.ModShift) | |
| 33 | + | before, ok := windowIn(set, "DP-1", 2) | |
| 34 | + | if !ok || !before.Floating || before.Rect.X != 0 { | |
| 35 | + | t.Fatalf("%+v", before) | |
| 36 | + | } | |
| 37 | + | // The workspace moves to the 400x200 output: the centre keeps its | |
| 38 | + | // fractions of the usable area (1/4, 1/2), the size stays. | |
| 39 | + | set = r.keyLanded(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | |
| 40 | + | after, ok := windowIn(set, "DP-2", 2) | |
| 41 | + | if !ok || after.Rect.W != before.Rect.W || after.Rect.X+after.Rect.W/2 != 100 || after.Rect.Y+after.Rect.H/2 != 100 { | |
| 42 | + | t.Fatalf("%+v", after) | |
| 43 | + | } | |
| 44 | + | }) | |
| 43 | 45 | } | |
| @@ -21,23 +21,25 @@ func windowIn(set []ports.Scene, output string, id ports.WindowID) (ports.SceneW | |||||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | func TestFreeFloatKeepsPlaceAcrossMonitors(t *testing.T) { | 23 | func TestFreeFloatKeepsPlaceAcrossMonitors(t *testing.T) { | ||
| 24 | - | r := startMulti(t, func(c *ports.Config) { c.Animations.On = false; c.Layout.Gaps = 0 }, left, right) | 24 | + | both(t, func(t *testing.T, animated bool) { |
| 25 | - | r.mapWindow(t, 1) | 25 | + | r := startLanding(t, animated, func(c *ports.Config) { c.Layout.Gaps = 0 }, left, right) |
| 26 | - | r.mapWindow(t, 2) | 26 | + | r.mapLanded(t, 1) |
| 27 | - | r.key(t, "space", ports.ModAlt|ports.ModShift) | 27 | + | r.mapLanded(t, 2) |
| 28 | - | // Nudge left twice: 100 px left of centre on a 200 px output stops at | 28 | + | r.keyLanded(t, "space", ports.ModAlt|ports.ModShift) |
| 29 | - | // the edge. | 29 | + | // Nudge left twice: 100 px left of centre on a 200 px output stops at |
| 30 | - | r.key(t, "Left", ports.ModAlt|ports.ModShift) | 30 | + | // the edge. |
| 31 | - | set := r.key(t, "Left", ports.ModAlt|ports.ModShift) | 31 | + | r.keyLanded(t, "Left", ports.ModAlt|ports.ModShift) |
| 32 | - | before, ok := windowIn(set, "DP-1", 2) | 32 | + | set := r.keyLanded(t, "Left", ports.ModAlt|ports.ModShift) |
| 33 | - | if !ok || !before.Floating || before.Rect.X != 0 { | 33 | + | before, ok := windowIn(set, "DP-1", 2) |
| 34 | - | t.Fatalf("%+v", before) | 34 | + | if !ok || !before.Floating || before.Rect.X != 0 { |
| 35 | - | } | 35 | + | t.Fatalf("%+v", before) |
| 36 | - | // The workspace moves to the 400x200 output: the centre keeps its | 36 | + | } |
| 37 | - | // fractions of the usable area (1/4, 1/2), the size stays. | 37 | + | // The workspace moves to the 400x200 output: the centre keeps its |
| 38 | - | set = r.key(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) | 38 | + | // fractions of the usable area (1/4, 1/2), the size stays. |
| 39 | - | after, ok := windowIn(set, "DP-2", 2) | 39 | + | set = r.keyLanded(t, "Right", ports.ModAlt|ports.ModCtrl|ports.ModShift) |
| 40 | - | if !ok || after.Rect.W != before.Rect.W || after.Rect.X+after.Rect.W/2 != 100 || after.Rect.Y+after.Rect.H/2 != 100 { | 40 | + | after, ok := windowIn(set, "DP-2", 2) |
| 41 | - | t.Fatalf("%+v", after) | 41 | + | if !ok || after.Rect.W != before.Rect.W || after.Rect.X+after.Rect.W/2 != 100 || after.Rect.Y+after.Rect.H/2 != 100 { |
| 42 | - | } | 42 | + | t.Fatalf("%+v", after) |
| 43 | + | } | |||
| 44 | + | }) | |||
| 43 | } | 45 | } | ||
M internal/core/screens_test.go +62 -58
| @@ -400,33 +400,35 @@ func TestMonitorActionsFollowGeometry(t *testing.T) { | |||
| 400 | 400 | } | |
| 401 | 401 | ||
| 402 | 402 | func TestUnplugMovesWorkspacesAndReplugReturnsThem(t *testing.T) { | |
| 403 | - | r := startMulti(t, animationsOff, left, right) | |
| 404 | - | r.mapWindow(t, 1) | |
| 405 | - | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 406 | - | r.mapWindow(t, 2) | |
| 407 | - | // Unplug DP-2 while it is focused: its workspace joins DP-1 below its own. | |
| 408 | - | r.output <- ports.OutputRemoved{Name: "DP-2"} | |
| 409 | - | set := receive(t, r.scenes) | |
| 410 | - | if len(set) != 1 || len(shown(set)["DP-1"]) != 1 || shown(set)["DP-1"][0] != 1 { | |
| 411 | - | t.Fatal(shown(set)) | |
| 412 | - | } | |
| 413 | - | // Cmd+2 shows the guest. | |
| 414 | - | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} | |
| 415 | - | set = receive(t, r.scenes) | |
| 416 | - | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt} | |
| 417 | - | if got := shown(set)["DP-1"]; len(got) != 1 || got[0] != 2 { | |
| 418 | - | t.Fatal(got) | |
| 419 | - | } | |
| 420 | - | // Replugged while on screen: the guest stays until the user leaves it. | |
| 421 | - | set = r.plug(t, right) | |
| 422 | - | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 2 || len(got["DP-2"]) != 0 { | |
| 423 | - | t.Fatal(got) | |
| 424 | - | } | |
| 425 | - | r.input <- ports.KeyEvent{Keysym: "1", Keycode: 2, Mods: ports.ModAlt, Pressed: true} | |
| 426 | - | set = receive(t, r.scenes) | |
| 427 | - | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 1 || len(got["DP-2"]) != 1 || got["DP-2"][0] != 2 { | |
| 428 | - | t.Fatal(got) | |
| 429 | - | } | |
| 403 | + | both(t, func(t *testing.T, animated bool) { | |
| 404 | + | r := startLanding(t, animated, nil, left, right) | |
| 405 | + | r.mapLanded(t, 1) | |
| 406 | + | r.keyLanded(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 407 | + | r.mapLanded(t, 2) | |
| 408 | + | // Unplug DP-2 while it is focused: its workspace joins DP-1 below its own. | |
| 409 | + | r.output <- ports.OutputRemoved{Name: "DP-2"} | |
| 410 | + | set := r.land(receive(t, r.scenes)) | |
| 411 | + | if len(set) != 1 || len(shown(set)["DP-1"]) != 1 || shown(set)["DP-1"][0] != 1 { | |
| 412 | + | t.Fatal(shown(set)) | |
| 413 | + | } | |
| 414 | + | // Cmd+2 shows the guest. | |
| 415 | + | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} | |
| 416 | + | set = r.land(receive(t, r.scenes)) | |
| 417 | + | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt} | |
| 418 | + | if got := shown(set)["DP-1"]; len(got) != 1 || got[0] != 2 { | |
| 419 | + | t.Fatal(got) | |
| 420 | + | } | |
| 421 | + | // Replugged while on screen: the guest stays until the user leaves it. | |
| 422 | + | set = r.land(r.plug(t, right)) | |
| 423 | + | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 2 || len(got["DP-2"]) != 0 { | |
| 424 | + | t.Fatal(got) | |
| 425 | + | } | |
| 426 | + | r.input <- ports.KeyEvent{Keysym: "1", Keycode: 2, Mods: ports.ModAlt, Pressed: true} | |
| 427 | + | set = r.land(receive(t, r.scenes)) | |
| 428 | + | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 1 || len(got["DP-2"]) != 1 || got["DP-2"][0] != 2 { | |
| 429 | + | t.Fatal(got) | |
| 430 | + | } | |
| 431 | + | }) | |
| 430 | 432 | } | |
| 431 | 433 | ||
| 432 | 434 | func TestReplugReturnsHiddenGuestAtOnce(t *testing.T) { | |
| @@ -763,38 +765,40 @@ func TestKeyboardScreenFocusSticksUntilPointerLeaves(t *testing.T) { | |||
| 763 | 765 | } | |
| 764 | 766 | ||
| 765 | 767 | func TestStateSnapshot(t *testing.T) { | |
| 766 | - | r := startMulti(t, animationsOff, left, right) | |
| 767 | - | r.client <- ports.WindowMapped{ID: 1, AppID: "foot", PID: 100} | |
| 768 | - | receive(t, r.scenes) | |
| 769 | - | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 770 | - | <-r.state // the channel holds the latest snapshot only | |
| 771 | - | r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200} | |
| 772 | - | receive(t, r.scenes) | |
| 773 | - | // Snapshots from before window 2 may still be queued: wait for it. | |
| 774 | - | st := receive(t, r.state) | |
| 775 | - | for len(st.Windows) < 2 { | |
| 768 | + | both(t, func(t *testing.T, animated bool) { | |
| 769 | + | r := startLanding(t, animated, nil, left, right) | |
| 770 | + | r.client <- ports.WindowMapped{ID: 1, AppID: "foot", PID: 100} | |
| 771 | + | r.land(receive(t, r.scenes)) | |
| 772 | + | r.keyLanded(t, "Right", ports.ModAlt|ports.ModCtrl) | |
| 773 | + | <-r.state // the channel holds the latest snapshot only | |
| 774 | + | r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200} | |
| 775 | + | r.land(receive(t, r.scenes)) | |
| 776 | + | // Snapshots from before window 2 may still be queued: wait for it. | |
| 777 | + | st := receive(t, r.state) | |
| 778 | + | for len(st.Windows) < 2 { | |
| 779 | + | st = receive(t, r.state) | |
| 780 | + | } | |
| 781 | + | want := ports.State{ | |
| 782 | + | Output: "DP-2", | |
| 783 | + | Outputs: []ports.OutputState{{Name: "DP-1", Active: 1, Count: 1, WorkspaceID: 1}, {Name: "DP-2", Active: 1, Count: 1, WorkspaceID: 2}}, | |
| 784 | + | Windows: []ports.WindowState{ | |
| 785 | + | {ID: 1, AppID: "foot", PID: 100, Output: "DP-1", Workspace: 1, WorkspaceID: 1, Visible: true}, | |
| 786 | + | {ID: 2, AppID: "firefox", PID: 200, Output: "DP-2", Workspace: 1, WorkspaceID: 2, Visible: true}, | |
| 787 | + | }, | |
| 788 | + | } | |
| 789 | + | want.Window = &want.Windows[1] | |
| 790 | + | if !reflect.DeepEqual(st, want) { | |
| 791 | + | t.Fatalf("got %+v\nwant %+v", st, want) | |
| 792 | + | } | |
| 793 | + | // Moving to workspace 2 of DP-2: two workspaces there, window 2 off screen. | |
| 794 | + | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} | |
| 795 | + | r.land(receive(t, r.scenes)) | |
| 776 | 796 | st = receive(t, r.state) | |
| 777 | - | } | |
| 778 | - | want := ports.State{ | |
| 779 | - | Output: "DP-2", | |
| 780 | - | Outputs: []ports.OutputState{{Name: "DP-1", Active: 1, Count: 1, WorkspaceID: 1}, {Name: "DP-2", Active: 1, Count: 1, WorkspaceID: 2}}, | |
| 781 | - | Windows: []ports.WindowState{ | |
| 782 | - | {ID: 1, AppID: "foot", PID: 100, Output: "DP-1", Workspace: 1, WorkspaceID: 1, Visible: true}, | |
| 783 | - | {ID: 2, AppID: "firefox", PID: 200, Output: "DP-2", Workspace: 1, WorkspaceID: 2, Visible: true}, | |
| 784 | - | }, | |
| 785 | - | } | |
| 786 | - | want.Window = &want.Windows[1] | |
| 787 | - | if !reflect.DeepEqual(st, want) { | |
| 788 | - | t.Fatalf("got %+v\nwant %+v", st, want) | |
| 789 | - | } | |
| 790 | - | // Moving to workspace 2 of DP-2: two workspaces there, window 2 off screen. | |
| 791 | - | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} | |
| 792 | - | receive(t, r.scenes) | |
| 793 | - | st = receive(t, r.state) | |
| 794 | - | // The window keeps its workspace ID; the output now shows the new one. | |
| 795 | - | if o := st.Outputs[1]; o.Name != "DP-2" || o.Active != 2 || o.Count != 2 || o.WorkspaceID == 0 || o.WorkspaceID == 2 || st.Windows[1].WorkspaceID != 2 || st.Windows[1].Visible || st.Window != nil { | |
| 796 | - | t.Fatalf("%+v", st) | |
| 797 | - | } | |
| 797 | + | // The window keeps its workspace ID; the output now shows the new one. | |
| 798 | + | if o := st.Outputs[1]; o.Name != "DP-2" || o.Active != 2 || o.Count != 2 || o.WorkspaceID == 0 || o.WorkspaceID == 2 || st.Windows[1].WorkspaceID != 2 || st.Windows[1].Visible || st.Window != nil { | |
| 799 | + | t.Fatalf("%+v", st) | |
| 800 | + | } | |
| 801 | + | }) | |
| 798 | 802 | } | |
| 799 | 803 | ||
| 800 | 804 | func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) { | |
| @@ -400,33 +400,35 @@ func TestMonitorActionsFollowGeometry(t *testing.T) { | |||||
| 400 | } | 400 | } | ||
| 401 | 401 | ||||
| 402 | func TestUnplugMovesWorkspacesAndReplugReturnsThem(t *testing.T) { | 402 | func TestUnplugMovesWorkspacesAndReplugReturnsThem(t *testing.T) { | ||
| 403 | - | r := startMulti(t, animationsOff, left, right) | 403 | + | both(t, func(t *testing.T, animated bool) { |
| 404 | - | r.mapWindow(t, 1) | 404 | + | r := startLanding(t, animated, nil, left, right) |
| 405 | - | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | 405 | + | r.mapLanded(t, 1) |
| 406 | - | r.mapWindow(t, 2) | 406 | + | r.keyLanded(t, "Right", ports.ModAlt|ports.ModCtrl) |
| 407 | - | // Unplug DP-2 while it is focused: its workspace joins DP-1 below its own. | 407 | + | r.mapLanded(t, 2) |
| 408 | - | r.output <- ports.OutputRemoved{Name: "DP-2"} | 408 | + | // Unplug DP-2 while it is focused: its workspace joins DP-1 below its own. |
| 409 | - | set := receive(t, r.scenes) | 409 | + | r.output <- ports.OutputRemoved{Name: "DP-2"} |
| 410 | - | if len(set) != 1 || len(shown(set)["DP-1"]) != 1 || shown(set)["DP-1"][0] != 1 { | 410 | + | set := r.land(receive(t, r.scenes)) |
| 411 | - | t.Fatal(shown(set)) | 411 | + | if len(set) != 1 || len(shown(set)["DP-1"]) != 1 || shown(set)["DP-1"][0] != 1 { |
| 412 | - | } | 412 | + | t.Fatal(shown(set)) |
| 413 | - | // Cmd+2 shows the guest. | 413 | + | } |
| 414 | - | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} | 414 | + | // Cmd+2 shows the guest. |
| 415 | - | set = receive(t, r.scenes) | 415 | + | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} |
| 416 | - | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt} | 416 | + | set = r.land(receive(t, r.scenes)) |
| 417 | - | if got := shown(set)["DP-1"]; len(got) != 1 || got[0] != 2 { | 417 | + | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt} |
| 418 | - | t.Fatal(got) | 418 | + | if got := shown(set)["DP-1"]; len(got) != 1 || got[0] != 2 { |
| 419 | - | } | 419 | + | t.Fatal(got) |
| 420 | - | // Replugged while on screen: the guest stays until the user leaves it. | 420 | + | } |
| 421 | - | set = r.plug(t, right) | 421 | + | // Replugged while on screen: the guest stays until the user leaves it. |
| 422 | - | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 2 || len(got["DP-2"]) != 0 { | 422 | + | set = r.land(r.plug(t, right)) |
| 423 | - | t.Fatal(got) | 423 | + | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 2 || len(got["DP-2"]) != 0 { |
| 424 | - | } | 424 | + | t.Fatal(got) |
| 425 | - | r.input <- ports.KeyEvent{Keysym: "1", Keycode: 2, Mods: ports.ModAlt, Pressed: true} | 425 | + | } |
| 426 | - | set = receive(t, r.scenes) | 426 | + | r.input <- ports.KeyEvent{Keysym: "1", Keycode: 2, Mods: ports.ModAlt, Pressed: true} |
| 427 | - | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 1 || len(got["DP-2"]) != 1 || got["DP-2"][0] != 2 { | 427 | + | set = r.land(receive(t, r.scenes)) |
| 428 | - | t.Fatal(got) | 428 | + | if got := shown(set); len(got["DP-1"]) != 1 || got["DP-1"][0] != 1 || len(got["DP-2"]) != 1 || got["DP-2"][0] != 2 { |
| 429 | - | } | 429 | + | t.Fatal(got) |
| 430 | + | } | |||
| 431 | + | }) | |||
| 430 | } | 432 | } | ||
| 431 | 433 | ||||
| 432 | func TestReplugReturnsHiddenGuestAtOnce(t *testing.T) { | 434 | func TestReplugReturnsHiddenGuestAtOnce(t *testing.T) { | ||
| @@ -763,38 +765,40 @@ func TestKeyboardScreenFocusSticksUntilPointerLeaves(t *testing.T) { | |||||
| 763 | } | 765 | } | ||
| 764 | 766 | ||||
| 765 | func TestStateSnapshot(t *testing.T) { | 767 | func TestStateSnapshot(t *testing.T) { | ||
| 766 | - | r := startMulti(t, animationsOff, left, right) | 768 | + | both(t, func(t *testing.T, animated bool) { |
| 767 | - | r.client <- ports.WindowMapped{ID: 1, AppID: "foot", PID: 100} | 769 | + | r := startLanding(t, animated, nil, left, right) |
| 768 | - | receive(t, r.scenes) | 770 | + | r.client <- ports.WindowMapped{ID: 1, AppID: "foot", PID: 100} |
| 769 | - | r.key(t, "Right", ports.ModAlt|ports.ModCtrl) | 771 | + | r.land(receive(t, r.scenes)) |
| 770 | - | <-r.state // the channel holds the latest snapshot only | 772 | + | r.keyLanded(t, "Right", ports.ModAlt|ports.ModCtrl) |
| 771 | - | r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200} | 773 | + | <-r.state // the channel holds the latest snapshot only |
| 772 | - | receive(t, r.scenes) | 774 | + | r.client <- ports.WindowMapped{ID: 2, AppID: "firefox", PID: 200} |
| 773 | - | // Snapshots from before window 2 may still be queued: wait for it. | 775 | + | r.land(receive(t, r.scenes)) |
| 774 | - | st := receive(t, r.state) | 776 | + | // Snapshots from before window 2 may still be queued: wait for it. |
| 775 | - | for len(st.Windows) < 2 { | 777 | + | st := receive(t, r.state) |
| 778 | + | for len(st.Windows) < 2 { | |||
| 779 | + | st = receive(t, r.state) | |||
| 780 | + | } | |||
| 781 | + | want := ports.State{ | |||
| 782 | + | Output: "DP-2", | |||
| 783 | + | Outputs: []ports.OutputState{{Name: "DP-1", Active: 1, Count: 1, WorkspaceID: 1}, {Name: "DP-2", Active: 1, Count: 1, WorkspaceID: 2}}, | |||
| 784 | + | Windows: []ports.WindowState{ | |||
| 785 | + | {ID: 1, AppID: "foot", PID: 100, Output: "DP-1", Workspace: 1, WorkspaceID: 1, Visible: true}, | |||
| 786 | + | {ID: 2, AppID: "firefox", PID: 200, Output: "DP-2", Workspace: 1, WorkspaceID: 2, Visible: true}, | |||
| 787 | + | }, | |||
| 788 | + | } | |||
| 789 | + | want.Window = &want.Windows[1] | |||
| 790 | + | if !reflect.DeepEqual(st, want) { | |||
| 791 | + | t.Fatalf("got %+v\nwant %+v", st, want) | |||
| 792 | + | } | |||
| 793 | + | // Moving to workspace 2 of DP-2: two workspaces there, window 2 off screen. | |||
| 794 | + | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} | |||
| 795 | + | r.land(receive(t, r.scenes)) | |||
| 776 | st = receive(t, r.state) | 796 | st = receive(t, r.state) | ||
| 777 | - | } | 797 | + | // The window keeps its workspace ID; the output now shows the new one. |
| 778 | - | want := ports.State{ | 798 | + | if o := st.Outputs[1]; o.Name != "DP-2" || o.Active != 2 || o.Count != 2 || o.WorkspaceID == 0 || o.WorkspaceID == 2 || st.Windows[1].WorkspaceID != 2 || st.Windows[1].Visible || st.Window != nil { |
| 779 | - | Output: "DP-2", | 799 | + | t.Fatalf("%+v", st) |
| 780 | - | Outputs: []ports.OutputState{{Name: "DP-1", Active: 1, Count: 1, WorkspaceID: 1}, {Name: "DP-2", Active: 1, Count: 1, WorkspaceID: 2}}, | 800 | + | } |
| 781 | - | Windows: []ports.WindowState{ | 801 | + | }) |
| 782 | - | {ID: 1, AppID: "foot", PID: 100, Output: "DP-1", Workspace: 1, WorkspaceID: 1, Visible: true}, | |||
| 783 | - | {ID: 2, AppID: "firefox", PID: 200, Output: "DP-2", Workspace: 1, WorkspaceID: 2, Visible: true}, | |||
| 784 | - | }, | |||
| 785 | - | } | |||
| 786 | - | want.Window = &want.Windows[1] | |||
| 787 | - | if !reflect.DeepEqual(st, want) { | |||
| 788 | - | t.Fatalf("got %+v\nwant %+v", st, want) | |||
| 789 | - | } | |||
| 790 | - | // Moving to workspace 2 of DP-2: two workspaces there, window 2 off screen. | |||
| 791 | - | r.input <- ports.KeyEvent{Keysym: "2", Keycode: 3, Mods: ports.ModAlt, Pressed: true} | |||
| 792 | - | receive(t, r.scenes) | |||
| 793 | - | st = receive(t, r.state) | |||
| 794 | - | // The window keeps its workspace ID; the output now shows the new one. | |||
| 795 | - | if o := st.Outputs[1]; o.Name != "DP-2" || o.Active != 2 || o.Count != 2 || o.WorkspaceID == 0 || o.WorkspaceID == 2 || st.Windows[1].WorkspaceID != 2 || st.Windows[1].Visible || st.Window != nil { | |||
| 796 | - | t.Fatalf("%+v", st) | |||
| 797 | - | } | |||
| 798 | } | 802 | } | ||
| 799 | 803 | ||||
| 800 | func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) { | 804 | func TestStateFollowsAppIDAndHiddenWorkspace(t *testing.T) { | ||
A internal/core/step_clock_test.go +117 -0
| @@ -0,0 +1,117 @@ | |||
| 1 | + | package core_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "sync" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |
| 10 | + | "github.com/stretchr/testify/mock" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | // stepClock is a clock the test moves: springs sample it, so a test lands | |
| 14 | + | // them by advancing it and sending page flips, never by sleeping. Its | |
| 15 | + | // timers never fire: frames come from the test. | |
| 16 | + | type stepClock struct { | |
| 17 | + | mu sync.Mutex | |
| 18 | + | now time.Time | |
| 19 | + | clock ports.Clock | |
| 20 | + | } | |
| 21 | + | ||
| 22 | + | func newStepClock(t *testing.T) *stepClock { | |
| 23 | + | t.Helper() | |
| 24 | + | s := &stepClock{now: time.Unix(100, 0)} | |
| 25 | + | clock := portsmocks.NewMockClock(t) | |
| 26 | + | clock.EXPECT().Now().RunAndReturn(func() time.Time { | |
| 27 | + | s.mu.Lock() | |
| 28 | + | defer s.mu.Unlock() | |
| 29 | + | return s.now | |
| 30 | + | }).Maybe() | |
| 31 | + | // One timer built before core runs: a mock created by core while the | |
| 32 | + | // test ends would race its own cleanup check. | |
| 33 | + | timer := portsmocks.NewMockTimer(t) | |
| 34 | + | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | |
| 35 | + | timer.EXPECT().Stop().Return(true).Maybe() | |
| 36 | + | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | |
| 37 | + | s.clock = clock | |
| 38 | + | return s | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func (s *stepClock) advance(d time.Duration) { | |
| 42 | + | s.mu.Lock() | |
| 43 | + | s.now = s.now.Add(d) | |
| 44 | + | s.mu.Unlock() | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | // settle moves the clock past every spring, flips each output and returns | |
| 48 | + | // the set core published for them (ok false: no spring ran, so nothing | |
| 49 | + | // moved). The last flip names no output: core takes it only after it | |
| 50 | + | // handled the previous ones, so the scene channel then holds their result. | |
| 51 | + | func (s *stepClock) settle(t *testing.T, frames chan ports.OutputFrame, scenes chan []ports.Scene, outputs ...string) ([]ports.Scene, bool) { | |
| 52 | + | t.Helper() | |
| 53 | + | s.advance(5 * time.Second) | |
| 54 | + | for _, o := range outputs { | |
| 55 | + | send(frames, ports.OutputFrame{Output: o}) | |
| 56 | + | } | |
| 57 | + | send(frames, ports.OutputFrame{Output: "sync"}) | |
| 58 | + | select { | |
| 59 | + | case set := <-scenes: | |
| 60 | + | return set, true | |
| 61 | + | default: | |
| 62 | + | return nil, false | |
| 63 | + | } | |
| 64 | + | } | |
| 65 | + | ||
| 66 | + | // landRig is a multiRig whose settle lands every running spring: with | |
| 67 | + | // animations on it runs on a clock the test moves (swipeRig) and sends the | |
| 68 | + | // page flips; with animations off it is the plain rig and land is the | |
| 69 | + | // identity. Each test body runs once per mode, so the off variant keeps | |
| 70 | + | // its meaning and the on variant asserts the same settled facts. | |
| 71 | + | type landRig struct { | |
| 72 | + | *multiRig | |
| 73 | + | land func([]ports.Scene) []ports.Scene | |
| 74 | + | } | |
| 75 | + | ||
| 76 | + | // startLanding runs core on the given outputs (gaps and terminal policy as | |
| 77 | + | // in startMulti) with animations on or off. | |
| 78 | + | func startLanding(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *landRig { | |
| 79 | + | t.Helper() | |
| 80 | + | if !animated { | |
| 81 | + | r := startMulti(t, func(c *ports.Config) { | |
| 82 | + | if edit != nil { | |
| 83 | + | edit(c) | |
| 84 | + | } | |
| 85 | + | c.Animations.On = false | |
| 86 | + | }, outs...) | |
| 87 | + | return &landRig{multiRig: r, land: func(set []ports.Scene) []ports.Scene { return set }} | |
| 88 | + | } | |
| 89 | + | defaults := altCmdDefaults() | |
| 90 | + | r := startSwipeOn(t, func(c *ports.Config) { | |
| 91 | + | c.Layout.Gaps, c.Terminal.AutoOpen = defaults.Layout.Gaps, defaults.Terminal.AutoOpen | |
| 92 | + | if edit != nil { | |
| 93 | + | edit(c) | |
| 94 | + | } | |
| 95 | + | c.Animations.On = true | |
| 96 | + | }, outs...) | |
| 97 | + | return &landRig{multiRig: r.multiRig, land: func(set []ports.Scene) []ports.Scene { return r.landed(t, set) }} | |
| 98 | + | } | |
| 99 | + | ||
| 100 | + | // both runs body with animations off, then on. | |
| 101 | + | func both(t *testing.T, body func(t *testing.T, animated bool)) { | |
| 102 | + | t.Helper() | |
| 103 | + | t.Run("off", func(t *testing.T) { body(t, false) }) | |
| 104 | + | t.Run("on", func(t *testing.T) { body(t, true) }) | |
| 105 | + | } | |
| 106 | + | ||
| 107 | + | // keyLanded presses a key and returns the settled scene set. | |
| 108 | + | func (r *landRig) keyLanded(t *testing.T, sym string, mods ports.Mods) []ports.Scene { | |
| 109 | + | t.Helper() | |
| 110 | + | return r.land(r.key(t, sym, mods)) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | // mapLanded maps a window and returns the settled scene set. | |
| 114 | + | func (r *landRig) mapLanded(t *testing.T, id ports.WindowID) []ports.Scene { | |
| 115 | + | t.Helper() | |
| 116 | + | return r.land(r.mapWindow(t, id)) | |
| 117 | + | } | |
| @@ -0,0 +1,117 @@ | |||||
| 1 | + | package core_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "sync" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |||
| 9 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 10 | + | "github.com/stretchr/testify/mock" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | // stepClock is a clock the test moves: springs sample it, so a test lands | |||
| 14 | + | // them by advancing it and sending page flips, never by sleeping. Its | |||
| 15 | + | // timers never fire: frames come from the test. | |||
| 16 | + | type stepClock struct { | |||
| 17 | + | mu sync.Mutex | |||
| 18 | + | now time.Time | |||
| 19 | + | clock ports.Clock | |||
| 20 | + | } | |||
| 21 | + | ||||
| 22 | + | func newStepClock(t *testing.T) *stepClock { | |||
| 23 | + | t.Helper() | |||
| 24 | + | s := &stepClock{now: time.Unix(100, 0)} | |||
| 25 | + | clock := portsmocks.NewMockClock(t) | |||
| 26 | + | clock.EXPECT().Now().RunAndReturn(func() time.Time { | |||
| 27 | + | s.mu.Lock() | |||
| 28 | + | defer s.mu.Unlock() | |||
| 29 | + | return s.now | |||
| 30 | + | }).Maybe() | |||
| 31 | + | // One timer built before core runs: a mock created by core while the | |||
| 32 | + | // test ends would race its own cleanup check. | |||
| 33 | + | timer := portsmocks.NewMockTimer(t) | |||
| 34 | + | timer.EXPECT().C().Return(make(chan time.Time)).Maybe() | |||
| 35 | + | timer.EXPECT().Stop().Return(true).Maybe() | |||
| 36 | + | clock.EXPECT().NewTimer(mock.Anything).Return(timer).Maybe() | |||
| 37 | + | s.clock = clock | |||
| 38 | + | return s | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func (s *stepClock) advance(d time.Duration) { | |||
| 42 | + | s.mu.Lock() | |||
| 43 | + | s.now = s.now.Add(d) | |||
| 44 | + | s.mu.Unlock() | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | // settle moves the clock past every spring, flips each output and returns | |||
| 48 | + | // the set core published for them (ok false: no spring ran, so nothing | |||
| 49 | + | // moved). The last flip names no output: core takes it only after it | |||
| 50 | + | // handled the previous ones, so the scene channel then holds their result. | |||
| 51 | + | func (s *stepClock) settle(t *testing.T, frames chan ports.OutputFrame, scenes chan []ports.Scene, outputs ...string) ([]ports.Scene, bool) { | |||
| 52 | + | t.Helper() | |||
| 53 | + | s.advance(5 * time.Second) | |||
| 54 | + | for _, o := range outputs { | |||
| 55 | + | send(frames, ports.OutputFrame{Output: o}) | |||
| 56 | + | } | |||
| 57 | + | send(frames, ports.OutputFrame{Output: "sync"}) | |||
| 58 | + | select { | |||
| 59 | + | case set := <-scenes: | |||
| 60 | + | return set, true | |||
| 61 | + | default: | |||
| 62 | + | return nil, false | |||
| 63 | + | } | |||
| 64 | + | } | |||
| 65 | + | ||||
| 66 | + | // landRig is a multiRig whose settle lands every running spring: with | |||
| 67 | + | // animations on it runs on a clock the test moves (swipeRig) and sends the | |||
| 68 | + | // page flips; with animations off it is the plain rig and land is the | |||
| 69 | + | // identity. Each test body runs once per mode, so the off variant keeps | |||
| 70 | + | // its meaning and the on variant asserts the same settled facts. | |||
| 71 | + | type landRig struct { | |||
| 72 | + | *multiRig | |||
| 73 | + | land func([]ports.Scene) []ports.Scene | |||
| 74 | + | } | |||
| 75 | + | ||||
| 76 | + | // startLanding runs core on the given outputs (gaps and terminal policy as | |||
| 77 | + | // in startMulti) with animations on or off. | |||
| 78 | + | func startLanding(t *testing.T, animated bool, edit func(*ports.Config), outs ...ports.OutputInfo) *landRig { | |||
| 79 | + | t.Helper() | |||
| 80 | + | if !animated { | |||
| 81 | + | r := startMulti(t, func(c *ports.Config) { | |||
| 82 | + | if edit != nil { | |||
| 83 | + | edit(c) | |||
| 84 | + | } | |||
| 85 | + | c.Animations.On = false | |||
| 86 | + | }, outs...) | |||
| 87 | + | return &landRig{multiRig: r, land: func(set []ports.Scene) []ports.Scene { return set }} | |||
| 88 | + | } | |||
| 89 | + | defaults := altCmdDefaults() | |||
| 90 | + | r := startSwipeOn(t, func(c *ports.Config) { | |||
| 91 | + | c.Layout.Gaps, c.Terminal.AutoOpen = defaults.Layout.Gaps, defaults.Terminal.AutoOpen | |||
| 92 | + | if edit != nil { | |||
| 93 | + | edit(c) | |||
| 94 | + | } | |||
| 95 | + | c.Animations.On = true | |||
| 96 | + | }, outs...) | |||
| 97 | + | return &landRig{multiRig: r.multiRig, land: func(set []ports.Scene) []ports.Scene { return r.landed(t, set) }} | |||
| 98 | + | } | |||
| 99 | + | ||||
| 100 | + | // both runs body with animations off, then on. | |||
| 101 | + | func both(t *testing.T, body func(t *testing.T, animated bool)) { | |||
| 102 | + | t.Helper() | |||
| 103 | + | t.Run("off", func(t *testing.T) { body(t, false) }) | |||
| 104 | + | t.Run("on", func(t *testing.T) { body(t, true) }) | |||
| 105 | + | } | |||
| 106 | + | ||||
| 107 | + | // keyLanded presses a key and returns the settled scene set. | |||
| 108 | + | func (r *landRig) keyLanded(t *testing.T, sym string, mods ports.Mods) []ports.Scene { | |||
| 109 | + | t.Helper() | |||
| 110 | + | return r.land(r.key(t, sym, mods)) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | // mapLanded maps a window and returns the settled scene set. | |||
| 114 | + | func (r *landRig) mapLanded(t *testing.T, id ports.WindowID) []ports.Scene { | |||
| 115 | + | t.Helper() | |||
| 116 | + | return r.land(r.mapWindow(t, id)) | |||
| 117 | + | } | |||
M internal/core/swipe_test.go +49 -1
| @@ -21,11 +21,19 @@ type swipeRig struct { | |||
| 21 | 21 | now time.Time | |
| 22 | 22 | // at is the device time of the next swipe event. | |
| 23 | 23 | at time.Duration | |
| 24 | + | // outs are the plugged outputs, for landed. | |
| 25 | + | outs []string | |
| 24 | 26 | } | |
| 25 | 27 | ||
| 26 | 28 | var wide = ports.OutputInfo{Name: "DP-1", Width: 800, Height: 600, RefreshMilli: 60000} | |
| 27 | 29 | ||
| 28 | 30 | func startSwipe(t *testing.T, edit func(*ports.Config)) *swipeRig { | |
| 31 | + | t.Helper() | |
| 32 | + | return startSwipeOn(t, edit, wide) | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // startSwipeOn is startSwipe with the given outputs plugged in order. | |
| 36 | + | func startSwipeOn(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *swipeRig { | |
| 29 | 37 | t.Helper() | |
| 30 | 38 | cfg := altCmdDefaults() | |
| 31 | 39 | cfg.Border.Width = 0 | |
| @@ -61,10 +69,24 @@ func startSwipe(t *testing.T, edit func(*ports.Config)) *swipeRig { | |||
| 61 | 69 | done := make(chan error, 1) | |
| 62 | 70 | go func() { done <- c.Run(ctx) }() | |
| 63 | 71 | t.Cleanup(func() { cancel(); <-done }) | |
| 64 | - | r.plug(t, wide) | |
| 72 | + | for _, o := range outs { | |
| 73 | + | r.plug(t, o) | |
| 74 | + | r.outs = append(r.outs, o.Name) | |
| 75 | + | } | |
| 65 | 76 | return r | |
| 66 | 77 | } | |
| 67 | 78 | ||
| 79 | + | // landed is the scene set once every spring an action started on any output | |
| 80 | + | // landed: the set itself when nothing animates (animations off, or an | |
| 81 | + | // action that moves nothing). | |
| 82 | + | func (r *swipeRig) landed(t *testing.T, set []ports.Scene) []ports.Scene { | |
| 83 | + | t.Helper() | |
| 84 | + | if landed, ok := r.settleAll(t, r.outs...); ok { | |
| 85 | + | return landed | |
| 86 | + | } | |
| 87 | + | return set | |
| 88 | + | } | |
| 89 | + | ||
| 68 | 90 | func (r *swipeRig) advance(d time.Duration) { | |
| 69 | 91 | r.mu.Lock() | |
| 70 | 92 | r.now = r.now.Add(d) | |
| @@ -105,6 +127,32 @@ func (r *swipeRig) settle(t *testing.T) ports.Scene { | |||
| 105 | 127 | return r.frame(t, 5*time.Second) | |
| 106 | 128 | } | |
| 107 | 129 | ||
| 130 | + | // flipAll moves the clock by d, sends a page flip for each output and | |
| 131 | + | // returns the latest scene set they published (ok false: none, nothing | |
| 132 | + | // animated). The last flip names no output: core takes it only after it | |
| 133 | + | // handled the previous ones, so the scene channel then holds their result. | |
| 134 | + | func (r *swipeRig) flipAll(t *testing.T, d time.Duration, outputs ...string) (set []ports.Scene, ok bool) { | |
| 135 | + | t.Helper() | |
| 136 | + | r.advance(d) | |
| 137 | + | for _, o := range outputs { | |
| 138 | + | send(r.frames, ports.OutputFrame{Output: o}) | |
| 139 | + | } | |
| 140 | + | send(r.frames, ports.OutputFrame{Output: "sync"}) | |
| 141 | + | select { | |
| 142 | + | case set = <-r.scenes: | |
| 143 | + | return set, true | |
| 144 | + | default: | |
| 145 | + | return nil, false | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | // settleAll moves the clock past every spring on the outputs and returns | |
| 150 | + | // the scene set that results (ok false: nothing was animating). | |
| 151 | + | func (r *swipeRig) settleAll(t *testing.T, outputs ...string) ([]ports.Scene, bool) { | |
| 152 | + | t.Helper() | |
| 153 | + | return r.flipAll(t, 5*time.Second, outputs...) | |
| 154 | + | } | |
| 155 | + | ||
| 108 | 156 | // flick sends a quick swipe of n updates of (dx, dy) and lifts the | |
| 109 | 157 | // fingers; it returns the scene right after the lift. | |
| 110 | 158 | func (r *swipeRig) flick(t *testing.T, n int, dx, dy float64) ports.Scene { | |
| @@ -21,11 +21,19 @@ type swipeRig struct { | |||||
| 21 | now time.Time | 21 | now time.Time | ||
| 22 | // at is the device time of the next swipe event. | 22 | // at is the device time of the next swipe event. | ||
| 23 | at time.Duration | 23 | at time.Duration | ||
| 24 | + | // outs are the plugged outputs, for landed. | |||
| 25 | + | outs []string | |||
| 24 | } | 26 | } | ||
| 25 | 27 | ||||
| 26 | var wide = ports.OutputInfo{Name: "DP-1", Width: 800, Height: 600, RefreshMilli: 60000} | 28 | var wide = ports.OutputInfo{Name: "DP-1", Width: 800, Height: 600, RefreshMilli: 60000} | ||
| 27 | 29 | ||||
| 28 | func startSwipe(t *testing.T, edit func(*ports.Config)) *swipeRig { | 30 | func startSwipe(t *testing.T, edit func(*ports.Config)) *swipeRig { | ||
| 31 | + | t.Helper() | |||
| 32 | + | return startSwipeOn(t, edit, wide) | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // startSwipeOn is startSwipe with the given outputs plugged in order. | |||
| 36 | + | func startSwipeOn(t *testing.T, edit func(*ports.Config), outs ...ports.OutputInfo) *swipeRig { | |||
| 29 | t.Helper() | 37 | t.Helper() | ||
| 30 | cfg := altCmdDefaults() | 38 | cfg := altCmdDefaults() | ||
| 31 | cfg.Border.Width = 0 | 39 | cfg.Border.Width = 0 | ||
| @@ -61,10 +69,24 @@ func startSwipe(t *testing.T, edit func(*ports.Config)) *swipeRig { | |||||
| 61 | done := make(chan error, 1) | 69 | done := make(chan error, 1) | ||
| 62 | go func() { done <- c.Run(ctx) }() | 70 | go func() { done <- c.Run(ctx) }() | ||
| 63 | t.Cleanup(func() { cancel(); <-done }) | 71 | t.Cleanup(func() { cancel(); <-done }) | ||
| 64 | - | r.plug(t, wide) | 72 | + | for _, o := range outs { |
| 73 | + | r.plug(t, o) | |||
| 74 | + | r.outs = append(r.outs, o.Name) | |||
| 75 | + | } | |||
| 65 | return r | 76 | return r | ||
| 66 | } | 77 | } | ||
| 67 | 78 | ||||
| 79 | + | // landed is the scene set once every spring an action started on any output | |||
| 80 | + | // landed: the set itself when nothing animates (animations off, or an | |||
| 81 | + | // action that moves nothing). | |||
| 82 | + | func (r *swipeRig) landed(t *testing.T, set []ports.Scene) []ports.Scene { | |||
| 83 | + | t.Helper() | |||
| 84 | + | if landed, ok := r.settleAll(t, r.outs...); ok { | |||
| 85 | + | return landed | |||
| 86 | + | } | |||
| 87 | + | return set | |||
| 88 | + | } | |||
| 89 | + | ||||
| 68 | func (r *swipeRig) advance(d time.Duration) { | 90 | func (r *swipeRig) advance(d time.Duration) { | ||
| 69 | r.mu.Lock() | 91 | r.mu.Lock() | ||
| 70 | r.now = r.now.Add(d) | 92 | r.now = r.now.Add(d) | ||
| @@ -105,6 +127,32 @@ func (r *swipeRig) settle(t *testing.T) ports.Scene { | |||||
| 105 | return r.frame(t, 5*time.Second) | 127 | return r.frame(t, 5*time.Second) | ||
| 106 | } | 128 | } | ||
| 107 | 129 | ||||
| 130 | + | // flipAll moves the clock by d, sends a page flip for each output and | |||
| 131 | + | // returns the latest scene set they published (ok false: none, nothing | |||
| 132 | + | // animated). The last flip names no output: core takes it only after it | |||
| 133 | + | // handled the previous ones, so the scene channel then holds their result. | |||
| 134 | + | func (r *swipeRig) flipAll(t *testing.T, d time.Duration, outputs ...string) (set []ports.Scene, ok bool) { | |||
| 135 | + | t.Helper() | |||
| 136 | + | r.advance(d) | |||
| 137 | + | for _, o := range outputs { | |||
| 138 | + | send(r.frames, ports.OutputFrame{Output: o}) | |||
| 139 | + | } | |||
| 140 | + | send(r.frames, ports.OutputFrame{Output: "sync"}) | |||
| 141 | + | select { | |||
| 142 | + | case set = <-r.scenes: | |||
| 143 | + | return set, true | |||
| 144 | + | default: | |||
| 145 | + | return nil, false | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | // settleAll moves the clock past every spring on the outputs and returns | |||
| 150 | + | // the scene set that results (ok false: nothing was animating). | |||
| 151 | + | func (r *swipeRig) settleAll(t *testing.T, outputs ...string) ([]ports.Scene, bool) { | |||
| 152 | + | t.Helper() | |||
| 153 | + | return r.flipAll(t, 5*time.Second, outputs...) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 108 | // flick sends a quick swipe of n updates of (dx, dy) and lifts the | 156 | // flick sends a quick swipe of n updates of (dx, dy) and lifts the | ||
| 109 | // fingers; it returns the scene right after the lift. | 157 | // fingers; it returns the scene right after the lift. | ||
| 110 | func (r *swipeRig) flick(t *testing.T, n int, dx, dy float64) ports.Scene { | 158 | func (r *swipeRig) flick(t *testing.T, n int, dx, dy float64) ports.Scene { | ||
M internal/core/transition_test.go +41 -0
| @@ -421,3 +421,44 @@ func TestDiscreteSwipeTransition(t *testing.T) { | |||
| 421 | 421 | t.Fatalf("settled 1 at %+v and 2 at %+v, want 1 beside 2 on halves", got1, got2) | |
| 422 | 422 | } | |
| 423 | 423 | } | |
| 424 | + | ||
| 425 | + | // A window unmapped while its rect motion runs leaves the scene at once, its | |
| 426 | + | // motion goes with it, the others keep theirs, and once those settle no | |
| 427 | + | // frame is wanted: a motion kept for a gone window would ask for frames with | |
| 428 | + | // nothing to move. | |
| 429 | + | func TestUnmapMidRectMotion(t *testing.T) { | |
| 430 | + | r := startSwipe(t, nil) | |
| 431 | + | threeColumns(t, r) | |
| 432 | + | // 3, focused, swaps with 2: both move. | |
| 433 | + | r.key(t, "Left", ports.ModAlt|ports.ModShift) | |
| 434 | + | mid := r.frame(t, 16*time.Millisecond) | |
| 435 | + | if x := rectNow(t, mid, 3).X; x <= 0 || x >= 400 { | |
| 436 | + | t.Fatalf("window 3 at %d on the first frame, want it moving", x) | |
| 437 | + | } | |
| 438 | + | moving := rectNow(t, mid, 2).X | |
| 439 | + | r.client <- ports.WindowUnmapped{ID: 3} | |
| 440 | + | s := scene(t, r.scenes) | |
| 441 | + | if _, ok := rectOf(s, 3); ok { | |
| 442 | + | t.Fatal("the unmapped window is still shown") | |
| 443 | + | } | |
| 444 | + | for _, w := range s.Windows { | |
| 445 | + | if w.ID == 3 { | |
| 446 | + | t.Fatalf("window 3 still in the scene: %+v", w) | |
| 447 | + | } | |
| 448 | + | } | |
| 449 | + | // The neighbour's own motion carries on from where it was drawn. | |
| 450 | + | if got := rectNow(t, s, 2).X; got != moving { | |
| 451 | + | t.Fatalf("window 2 jumped from %d to %d on the unmap", moving, got) | |
| 452 | + | } | |
| 453 | + | if got := rectNow(t, r.frame(t, 16*time.Millisecond), 2).X; got == moving { | |
| 454 | + | t.Fatalf("window 2 stopped at %d: its motion died with the unmapped window", got) | |
| 455 | + | } | |
| 456 | + | s = r.settle(t) | |
| 457 | + | if _, ok := rectOf(s, 3); ok { | |
| 458 | + | t.Fatal("the unmapped window came back") | |
| 459 | + | } | |
| 460 | + | if got := rectNow(t, s, 2).X; got != 400 { | |
| 461 | + | t.Fatalf("window 2 settled at %d, want 400", got) | |
| 462 | + | } | |
| 463 | + | r.noFrameScene(t) | |
| 464 | + | } | |
| @@ -421,3 +421,44 @@ func TestDiscreteSwipeTransition(t *testing.T) { | |||||
| 421 | t.Fatalf("settled 1 at %+v and 2 at %+v, want 1 beside 2 on halves", got1, got2) | 421 | t.Fatalf("settled 1 at %+v and 2 at %+v, want 1 beside 2 on halves", got1, got2) | ||
| 422 | } | 422 | } | ||
| 423 | } | 423 | } | ||
| 424 | + | ||||
| 425 | + | // A window unmapped while its rect motion runs leaves the scene at once, its | |||
| 426 | + | // motion goes with it, the others keep theirs, and once those settle no | |||
| 427 | + | // frame is wanted: a motion kept for a gone window would ask for frames with | |||
| 428 | + | // nothing to move. | |||
| 429 | + | func TestUnmapMidRectMotion(t *testing.T) { | |||
| 430 | + | r := startSwipe(t, nil) | |||
| 431 | + | threeColumns(t, r) | |||
| 432 | + | // 3, focused, swaps with 2: both move. | |||
| 433 | + | r.key(t, "Left", ports.ModAlt|ports.ModShift) | |||
| 434 | + | mid := r.frame(t, 16*time.Millisecond) | |||
| 435 | + | if x := rectNow(t, mid, 3).X; x <= 0 || x >= 400 { | |||
| 436 | + | t.Fatalf("window 3 at %d on the first frame, want it moving", x) | |||
| 437 | + | } | |||
| 438 | + | moving := rectNow(t, mid, 2).X | |||
| 439 | + | r.client <- ports.WindowUnmapped{ID: 3} | |||
| 440 | + | s := scene(t, r.scenes) | |||
| 441 | + | if _, ok := rectOf(s, 3); ok { | |||
| 442 | + | t.Fatal("the unmapped window is still shown") | |||
| 443 | + | } | |||
| 444 | + | for _, w := range s.Windows { | |||
| 445 | + | if w.ID == 3 { | |||
| 446 | + | t.Fatalf("window 3 still in the scene: %+v", w) | |||
| 447 | + | } | |||
| 448 | + | } | |||
| 449 | + | // The neighbour's own motion carries on from where it was drawn. | |||
| 450 | + | if got := rectNow(t, s, 2).X; got != moving { | |||
| 451 | + | t.Fatalf("window 2 jumped from %d to %d on the unmap", moving, got) | |||
| 452 | + | } | |||
| 453 | + | if got := rectNow(t, r.frame(t, 16*time.Millisecond), 2).X; got == moving { | |||
| 454 | + | t.Fatalf("window 2 stopped at %d: its motion died with the unmapped window", got) | |||
| 455 | + | } | |||
| 456 | + | s = r.settle(t) | |||
| 457 | + | if _, ok := rectOf(s, 3); ok { | |||
| 458 | + | t.Fatal("the unmapped window came back") | |||
| 459 | + | } | |||
| 460 | + | if got := rectNow(t, s, 2).X; got != 400 { | |||
| 461 | + | t.Fatalf("window 2 settled at %d, want 400", got) | |||
| 462 | + | } | |||
| 463 | + | r.noFrameScene(t) | |||
| 464 | + | } | |||
M internal/core/workspace_size_test.go +50 -38
| @@ -12,7 +12,15 @@ var ultrawide = ports.OutputInfo{Name: "DP-3", Make: "Acme", Model: "W", Serial: | |||
| 12 | 12 | // bound to Alt+w, and shows it. | |
| 13 | 13 | func sizedRig(t *testing.T, overflow string, edit func(*ports.Config)) (*multiRig, ports.Scene) { | |
| 14 | 14 | t.Helper() | |
| 15 | - | r := startMulti(t, func(c *ports.Config) { | |
| 15 | + | r, s := sizedLanding(t, false, overflow, edit) | |
| 16 | + | return r.multiRig, s | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // sizedLanding is sizedRig with animations on or off; the scene it returns | |
| 20 | + | // and every one of its keyLanded and mapLanded are settled. | |
| 21 | + | func sizedLanding(t *testing.T, animated bool, overflow string, edit func(*ports.Config)) (*landRig, ports.Scene) { | |
| 22 | + | t.Helper() | |
| 23 | + | r := startLanding(t, animated, func(c *ports.Config) { | |
| 16 | 24 | c.Layout.Gaps = 0 | |
| 17 | 25 | c.Layout.MaxColumns = 2 | |
| 18 | 26 | c.Workspaces = []ports.WorkspaceConfig{{Name: "focus", Size: [2]int{1920, 1080}, LayoutRules: ports.LayoutRules{Overflow: overflow}}} | |
| @@ -21,7 +29,7 @@ func sizedRig(t *testing.T, overflow string, edit func(*ports.Config)) (*multiRi | |||
| 21 | 29 | edit(c) | |
| 22 | 30 | } | |
| 23 | 31 | }, ultrawide) | |
| 24 | - | return r, r.key(t, "w", ports.ModAlt)[0] | |
| 32 | + | return r, r.keyLanded(t, "w", ports.ModAlt)[0] | |
| 25 | 33 | } | |
| 26 | 34 | ||
| 27 | 35 | var frame = ports.Rect{X: 760, Y: 180, W: 1920, H: 1080} | |
| @@ -73,48 +81,52 @@ func TestWorkspaceSizeFixedColumnsFillFrame(t *testing.T) { | |||
| 73 | 81 | } | |
| 74 | 82 | ||
| 75 | 83 | func TestWorkspaceSizeFullscreenFillsFrame(t *testing.T) { | |
| 76 | - | r, _ := sizedRig(t, "scroll", animationsOff) | |
| 77 | - | r.mapWindow(t, 1) | |
| 78 | - | r.mapWindow(t, 2) | |
| 79 | - | s := r.key(t, "f", ports.ModAlt|ports.ModShift)[0] | |
| 80 | - | full := 0 | |
| 81 | - | for _, w := range s.Windows { | |
| 82 | - | if w.Fullscreen { | |
| 83 | - | full++ | |
| 84 | - | if w.Rect != frame { | |
| 85 | - | t.Fatalf("%+v", w) | |
| 84 | + | both(t, func(t *testing.T, animated bool) { | |
| 85 | + | r, _ := sizedLanding(t, animated, "scroll", nil) | |
| 86 | + | r.mapLanded(t, 1) | |
| 87 | + | r.mapLanded(t, 2) | |
| 88 | + | s := r.keyLanded(t, "f", ports.ModAlt|ports.ModShift)[0] | |
| 89 | + | full := 0 | |
| 90 | + | for _, w := range s.Windows { | |
| 91 | + | if w.Fullscreen { | |
| 92 | + | full++ | |
| 93 | + | if w.Rect != frame { | |
| 94 | + | t.Fatalf("%+v", w) | |
| 95 | + | } | |
| 86 | 96 | } | |
| 87 | 97 | } | |
| 88 | - | } | |
| 89 | - | if full != 1 { | |
| 90 | - | t.Fatal(s.Windows) | |
| 91 | - | } | |
| 98 | + | if full != 1 { | |
| 99 | + | t.Fatal(s.Windows) | |
| 100 | + | } | |
| 101 | + | }) | |
| 92 | 102 | } | |
| 93 | 103 | ||
| 94 | 104 | func TestWorkspaceSizeFixedFullscreenKeepsFrame(t *testing.T) { | |
| 95 | - | r, _ := sizedRig(t, "fixed", animationsOff) | |
| 96 | - | r.mapWindow(t, 1) | |
| 97 | - | r.mapWindow(t, 2) | |
| 98 | - | s := r.key(t, "f", ports.ModAlt|ports.ModShift)[0] | |
| 99 | - | if s.WorkspaceClip != frame { | |
| 100 | - | t.Fatalf("fullscreen clip %+v", s.WorkspaceClip) | |
| 101 | - | } | |
| 102 | - | for _, w := range s.Windows { | |
| 103 | - | if w.Fullscreen && w.Rect != frame { | |
| 104 | - | t.Fatalf("%+v", w) | |
| 105 | + | both(t, func(t *testing.T, animated bool) { | |
| 106 | + | r, _ := sizedLanding(t, animated, "fixed", nil) | |
| 107 | + | r.mapLanded(t, 1) | |
| 108 | + | r.mapLanded(t, 2) | |
| 109 | + | s := r.keyLanded(t, "f", ports.ModAlt|ports.ModShift)[0] | |
| 110 | + | if s.WorkspaceClip != frame { | |
| 111 | + | t.Fatalf("fullscreen clip %+v", s.WorkspaceClip) | |
| 105 | 112 | } | |
| 106 | - | } | |
| 107 | - | // Reload keeps the size; removing it frees the workspace. | |
| 108 | - | cfg := r.cfg | |
| 109 | - | r.reload <- ports.ConfigChanged{Config: cfg} | |
| 110 | - | if s := receive(t, r.scenes)[0]; s.WorkspaceClip != frame { | |
| 111 | - | t.Fatalf("after reload %+v", s.WorkspaceClip) | |
| 112 | - | } | |
| 113 | - | cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", LayoutRules: ports.LayoutRules{Overflow: "fixed"}}} | |
| 114 | - | r.reload <- ports.ConfigChanged{Config: cfg} | |
| 115 | - | if s := receive(t, r.scenes)[0]; s.WorkspaceClip != (ports.Rect{}) { | |
| 116 | - | t.Fatalf("after size removal %+v", s.WorkspaceClip) | |
| 117 | - | } | |
| 113 | + | for _, w := range s.Windows { | |
| 114 | + | if w.Fullscreen && w.Rect != frame { | |
| 115 | + | t.Fatalf("%+v", w) | |
| 116 | + | } | |
| 117 | + | } | |
| 118 | + | // Reload keeps the size; removing it frees the workspace. | |
| 119 | + | cfg := r.cfg | |
| 120 | + | r.reload <- ports.ConfigChanged{Config: cfg} | |
| 121 | + | if s := r.land(receive(t, r.scenes))[0]; s.WorkspaceClip != frame { | |
| 122 | + | t.Fatalf("after reload %+v", s.WorkspaceClip) | |
| 123 | + | } | |
| 124 | + | cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", LayoutRules: ports.LayoutRules{Overflow: "fixed"}}} | |
| 125 | + | r.reload <- ports.ConfigChanged{Config: cfg} | |
| 126 | + | if s := r.land(receive(t, r.scenes))[0]; s.WorkspaceClip != (ports.Rect{}) { | |
| 127 | + | t.Fatalf("after size removal %+v", s.WorkspaceClip) | |
| 128 | + | } | |
| 129 | + | }) | |
| 118 | 130 | } | |
| 119 | 131 | ||
| 120 | 132 | func TestWorkspaceSizeFloatsContained(t *testing.T) { | |
| @@ -12,7 +12,15 @@ var ultrawide = ports.OutputInfo{Name: "DP-3", Make: "Acme", Model: "W", Serial: | |||||
| 12 | // bound to Alt+w, and shows it. | 12 | // bound to Alt+w, and shows it. | ||
| 13 | func sizedRig(t *testing.T, overflow string, edit func(*ports.Config)) (*multiRig, ports.Scene) { | 13 | func sizedRig(t *testing.T, overflow string, edit func(*ports.Config)) (*multiRig, ports.Scene) { | ||
| 14 | t.Helper() | 14 | t.Helper() | ||
| 15 | - | r := startMulti(t, func(c *ports.Config) { | 15 | + | r, s := sizedLanding(t, false, overflow, edit) |
| 16 | + | return r.multiRig, s | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // sizedLanding is sizedRig with animations on or off; the scene it returns | |||
| 20 | + | // and every one of its keyLanded and mapLanded are settled. | |||
| 21 | + | func sizedLanding(t *testing.T, animated bool, overflow string, edit func(*ports.Config)) (*landRig, ports.Scene) { | |||
| 22 | + | t.Helper() | |||
| 23 | + | r := startLanding(t, animated, func(c *ports.Config) { | |||
| 16 | c.Layout.Gaps = 0 | 24 | c.Layout.Gaps = 0 | ||
| 17 | c.Layout.MaxColumns = 2 | 25 | c.Layout.MaxColumns = 2 | ||
| 18 | c.Workspaces = []ports.WorkspaceConfig{{Name: "focus", Size: [2]int{1920, 1080}, LayoutRules: ports.LayoutRules{Overflow: overflow}}} | 26 | c.Workspaces = []ports.WorkspaceConfig{{Name: "focus", Size: [2]int{1920, 1080}, LayoutRules: ports.LayoutRules{Overflow: overflow}}} | ||
| @@ -21,7 +29,7 @@ func sizedRig(t *testing.T, overflow string, edit func(*ports.Config)) (*multiRi | |||||
| 21 | edit(c) | 29 | edit(c) | ||
| 22 | } | 30 | } | ||
| 23 | }, ultrawide) | 31 | }, ultrawide) | ||
| 24 | - | return r, r.key(t, "w", ports.ModAlt)[0] | 32 | + | return r, r.keyLanded(t, "w", ports.ModAlt)[0] |
| 25 | } | 33 | } | ||
| 26 | 34 | ||||
| 27 | var frame = ports.Rect{X: 760, Y: 180, W: 1920, H: 1080} | 35 | var frame = ports.Rect{X: 760, Y: 180, W: 1920, H: 1080} | ||
| @@ -73,48 +81,52 @@ func TestWorkspaceSizeFixedColumnsFillFrame(t *testing.T) { | |||||
| 73 | } | 81 | } | ||
| 74 | 82 | ||||
| 75 | func TestWorkspaceSizeFullscreenFillsFrame(t *testing.T) { | 83 | func TestWorkspaceSizeFullscreenFillsFrame(t *testing.T) { | ||
| 76 | - | r, _ := sizedRig(t, "scroll", animationsOff) | 84 | + | both(t, func(t *testing.T, animated bool) { |
| 77 | - | r.mapWindow(t, 1) | 85 | + | r, _ := sizedLanding(t, animated, "scroll", nil) |
| 78 | - | r.mapWindow(t, 2) | 86 | + | r.mapLanded(t, 1) |
| 79 | - | s := r.key(t, "f", ports.ModAlt|ports.ModShift)[0] | 87 | + | r.mapLanded(t, 2) |
| 80 | - | full := 0 | 88 | + | s := r.keyLanded(t, "f", ports.ModAlt|ports.ModShift)[0] |
| 81 | - | for _, w := range s.Windows { | 89 | + | full := 0 |
| 82 | - | if w.Fullscreen { | 90 | + | for _, w := range s.Windows { |
| 83 | - | full++ | 91 | + | if w.Fullscreen { |
| 84 | - | if w.Rect != frame { | 92 | + | full++ |
| 85 | - | t.Fatalf("%+v", w) | 93 | + | if w.Rect != frame { |
| 94 | + | t.Fatalf("%+v", w) | |||
| 95 | + | } | |||
| 86 | } | 96 | } | ||
| 87 | } | 97 | } | ||
| 88 | - | } | 98 | + | if full != 1 { |
| 89 | - | if full != 1 { | 99 | + | t.Fatal(s.Windows) |
| 90 | - | t.Fatal(s.Windows) | 100 | + | } |
| 91 | - | } | 101 | + | }) |
| 92 | } | 102 | } | ||
| 93 | 103 | ||||
| 94 | func TestWorkspaceSizeFixedFullscreenKeepsFrame(t *testing.T) { | 104 | func TestWorkspaceSizeFixedFullscreenKeepsFrame(t *testing.T) { | ||
| 95 | - | r, _ := sizedRig(t, "fixed", animationsOff) | 105 | + | both(t, func(t *testing.T, animated bool) { |
| 96 | - | r.mapWindow(t, 1) | 106 | + | r, _ := sizedLanding(t, animated, "fixed", nil) |
| 97 | - | r.mapWindow(t, 2) | 107 | + | r.mapLanded(t, 1) |
| 98 | - | s := r.key(t, "f", ports.ModAlt|ports.ModShift)[0] | 108 | + | r.mapLanded(t, 2) |
| 99 | - | if s.WorkspaceClip != frame { | 109 | + | s := r.keyLanded(t, "f", ports.ModAlt|ports.ModShift)[0] |
| 100 | - | t.Fatalf("fullscreen clip %+v", s.WorkspaceClip) | 110 | + | if s.WorkspaceClip != frame { |
| 101 | - | } | 111 | + | t.Fatalf("fullscreen clip %+v", s.WorkspaceClip) |
| 102 | - | for _, w := range s.Windows { | |||
| 103 | - | if w.Fullscreen && w.Rect != frame { | |||
| 104 | - | t.Fatalf("%+v", w) | |||
| 105 | } | 112 | } | ||
| 106 | - | } | 113 | + | for _, w := range s.Windows { |
| 107 | - | // Reload keeps the size; removing it frees the workspace. | 114 | + | if w.Fullscreen && w.Rect != frame { |
| 108 | - | cfg := r.cfg | 115 | + | t.Fatalf("%+v", w) |
| 109 | - | r.reload <- ports.ConfigChanged{Config: cfg} | 116 | + | } |
| 110 | - | if s := receive(t, r.scenes)[0]; s.WorkspaceClip != frame { | 117 | + | } |
| 111 | - | t.Fatalf("after reload %+v", s.WorkspaceClip) | 118 | + | // Reload keeps the size; removing it frees the workspace. |
| 112 | - | } | 119 | + | cfg := r.cfg |
| 113 | - | cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", LayoutRules: ports.LayoutRules{Overflow: "fixed"}}} | 120 | + | r.reload <- ports.ConfigChanged{Config: cfg} |
| 114 | - | r.reload <- ports.ConfigChanged{Config: cfg} | 121 | + | if s := r.land(receive(t, r.scenes))[0]; s.WorkspaceClip != frame { |
| 115 | - | if s := receive(t, r.scenes)[0]; s.WorkspaceClip != (ports.Rect{}) { | 122 | + | t.Fatalf("after reload %+v", s.WorkspaceClip) |
| 116 | - | t.Fatalf("after size removal %+v", s.WorkspaceClip) | 123 | + | } |
| 117 | - | } | 124 | + | cfg.Workspaces = []ports.WorkspaceConfig{{Name: "focus", LayoutRules: ports.LayoutRules{Overflow: "fixed"}}} |
| 125 | + | r.reload <- ports.ConfigChanged{Config: cfg} | |||
| 126 | + | if s := r.land(receive(t, r.scenes))[0]; s.WorkspaceClip != (ports.Rect{}) { | |||
| 127 | + | t.Fatalf("after size removal %+v", s.WorkspaceClip) | |||
| 128 | + | } | |||
| 129 | + | }) | |||
| 118 | } | 130 | } | ||
| 119 | 131 | ||||
| 120 | func TestWorkspaceSizeFloatsContained(t *testing.T) { | 132 | func TestWorkspaceSizeFloatsContained(t *testing.T) { | ||