1package core_test
2
3import (
4 "math/rand"
5 "testing"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10// Published scenes, not just layouts: exercise the real owner loop, binds,
11// monitor transfers, unmaps, and both kinds of overflow with a fixed seed.
12func TestBorderSceneInvariants(t *testing.T) {
13 for _, overflow := range []string{"fixed", "scroll"} {
14 for _, gap := range []int{0, 6} {
15 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 c.Border.Width = 2
19 c.Layout.MaxColumns = 3
20 c.Layout.Overflow = overflow
21 c.Layout.Gaps = gap
22 }, left, right)
23 var ids []ports.WindowID
24 next := ports.WindowID(1)
25 // A single keypress may publish a scene even when the action is
26 // inapplicable; check every resulting scene independently.
27 keys := []struct {
28 sym string
29 mods ports.Mods
30 }{
31 {"Left", ports.ModAlt | ports.ModShift}, {"Right", ports.ModAlt | ports.ModShift},
32 {"Left", ports.ModAlt}, {"Right", ports.ModAlt},
33 {"Left", ports.ModAlt | ports.ModCtrl}, {"Right", ports.ModAlt | ports.ModCtrl},
34 {"Left", ports.ModAlt | ports.ModCtrl | ports.ModShift}, {"Right", ports.ModAlt | ports.ModCtrl | ports.ModShift},
35 {"bracketleft", ports.ModAlt}, {"bracketright", ports.ModAlt},
36 {"f", ports.ModAlt}, {"f", ports.ModAlt | ports.ModShift},
37 {"s", ports.ModAlt}, {"S", ports.ModAlt | ports.ModShift}, {"Up", ports.ModAlt}, {"Down", ports.ModAlt},
38 }
39 for step := range 180 {
40 var scenes []ports.Scene
41 switch {
42 case len(ids) < 2 || rng.Intn(5) == 0 && len(ids) < 12:
43 scenes = r.mapWindow(t, next)
44 ids = append(ids, next)
45 next++
46 case rng.Intn(9) == 0:
47 i := rng.Intn(len(ids))
48 r.client <- ports.WindowUnmapped{ID: ids[i]}
49 scenes = receive(t, r.scenes)
50 ids = append(ids[:i], ids[i+1:]...)
51 default:
52 k := keys[rng.Intn(len(keys))]
53 scenes = r.key(t, k.sym, k.mods)
54 }
55 checkBorderScenes(t, step, gap, scenes)
56 }
57 })
58 }
59 }
60}
61
62func checkBorderScenes(t *testing.T, step, gap int, scenes []ports.Scene) {
63 t.Helper()
64 focused := 0
65 for _, s := range scenes {
66 checkBorderScene(t, step, gap, s)
67 for _, w := range s.Windows {
68 if w.Focused && !w.Hidden && !w.Popup && w.Rect.Overlaps(ports.Rect{W: s.OutputWidth, H: s.OutputHeight}) {
69 focused++
70 }
71 }
72 }
73 if focused > 1 {
74 t.Fatalf("step %d: focused windows on multiple outputs: %+v", step, scenes)
75 }
76}
77
78func checkBorderScene(t *testing.T, step, gap int, s ports.Scene) {
79 t.Helper()
80 o := ports.Rect{W: s.OutputWidth, H: s.OutputHeight}
81 var focus *ports.SceneWindow
82 tiles := 0
83 for i := range s.Windows {
84 w := &s.Windows[i]
85 if w.Popup || w.Hidden || !w.Rect.Overlaps(o) {
86 continue
87 }
88 if w.Focused {
89 if focus != nil {
90 t.Fatalf("step %d %s: two focused windows: %+v", step, s.Output, s.Windows)
91 }
92 focus = w
93 }
94 if !w.Floating && !w.Fullscreen {
95 tiles++
96 }
97 if w.Fullscreen && w.Inset != 0 {
98 t.Fatalf("step %d %s: fullscreen inset: %+v", step, s.Output, w)
99 }
100 if !w.Floating && !w.Fullscreen {
101 if got, want := w.Inset, wantInset(s, *w, gap); got != want {
102 t.Fatalf("step %d %s: tile %d inset %v, want %v; scene %+v", step, s.Output, w.ID, got, want, s)
103 }
104 }
105 }
106 if tiles <= 1 {
107 for _, w := range s.Windows {
108 if !w.Floating && !w.Popup && !w.Hidden && w.Rect.Overlaps(o) && w.Inset != 0 {
109 t.Fatalf("step %d %s: lone tile reserves a border: %+v; scene %+v", step, s.Output, w, s)
110 }
111 }
112 }
113 for _, sep := range s.Separators {
114 r := sep.Rect
115 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 {
116 t.Fatalf("step %d %s: line outside output: %+v; scene %+v", step, s.Output, sep, s)
117 }
118 if sep.Window != 0 {
119 found := false
120 for _, w := range s.Windows {
121 if w.ID == sep.Window && w.Floating && !w.Fullscreen && !w.Hidden && w.Rect.Overlaps(r) {
122 found = true
123 }
124 }
125 if !found {
126 t.Fatalf("step %d %s: line belongs to invisible float: %+v; scene %+v", step, s.Output, sep, s)
127 }
128 } else if tiles < 2 {
129 t.Fatalf("step %d %s: lone tile has shared line: %+v; scene %+v", step, s.Output, sep, s)
130 }
131 if sep.Active && (focus == nil || focus.Fullscreen || sep.Window != 0 && sep.Window != focus.ID || sep.Window == 0 && focus.Floating) {
132 t.Fatalf("step %d %s: lit line without matching focused tile/float: %+v; scene %+v", step, s.Output, sep, s)
133 }
134 }
135 // A focused tile with a visible neighbor lights at least one line
136 // touching it.
137 if focus == nil || focus.Floating || focus.Fullscreen || neighborSides(s, *focus, gap) == 0 {
138 return
139 }
140 near := ports.Rect{X: focus.Rect.X - 2, Y: focus.Rect.Y - 2, W: focus.Rect.W + 4, H: focus.Rect.H + 4}
141 for _, sep := range s.Separators {
142 if sep.Active && sep.Window == 0 && sep.Rect.Overlaps(near) {
143 return
144 }
145 }
146 t.Fatalf("step %d %s: focused tile %d has neighbors but no lit line; scene %+v", step, s.Output, focus.ID, s)
147}
148
149// neighborSides are the sides of tile w facing another visible tile across
150// gap, with the shared edge strictly inside the output.
151func neighborSides(s ports.Scene, w ports.SceneWindow, gap int) ports.Sides {
152 o := ports.Rect{W: s.OutputWidth, H: s.OutputHeight}
153 var sides ports.Sides
154 for _, other := range s.Windows {
155 if other.ID == w.ID || other.Popup || other.Floating || other.Fullscreen || other.Hidden || !other.Rect.Overlaps(o) {
156 continue
157 }
158 a, b := w.Rect, other.Rect
159 rows := a.Y < b.Y+b.H && b.Y < a.Y+a.H
160 cols := a.X < b.X+b.W && b.X < a.X+a.W
161 switch {
162 case rows && a.X+a.W+gap == b.X && a.X+a.W > 0 && a.X+a.W < o.W:
163 sides |= ports.SideRight
164 case rows && b.X+b.W+gap == a.X && a.X > 0 && a.X < o.W:
165 sides |= ports.SideLeft
166 case cols && a.Y+a.H+gap == b.Y && a.Y+a.H > 0 && a.Y+a.H < o.H:
167 sides |= ports.SideBottom
168 case cols && b.Y+b.H+gap == a.Y && a.Y > 0 && a.Y < o.H:
169 sides |= ports.SideTop
170 }
171 }
172 return sides
173}
174
175// wantInset is the exact inset of a visible tile: every neighbor side with
176// gaps, only the right and bottom (shared-line owner) sides without.
177func wantInset(s ports.Scene, w ports.SceneWindow, gap int) ports.Sides {
178 sides := neighborSides(s, w, gap)
179 if gap == 0 {
180 sides &= ports.SideRight | ports.SideBottom
181 }
182 return sides
183}
184
185// Regression for the original two-monitor transfer: when the last tile
186// comes back, neither the old output nor the new one has a shared line.
187func TestBorderMoveBackAlone(t *testing.T) {
188 r := startMulti(t, func(c *ports.Config) {
189 c.Border.Width = 2
190 c.Layout.MaxColumns = 3
191 c.Layout.Overflow = "fixed"
192 }, left, right)
193 r.key(t, "Right", ports.ModAlt|ports.ModCtrl)
194 r.mapWindow(t, 1)
195 for _, sym := range []string{"Left", "Right"} {
196 scenes := r.key(t, sym, ports.ModAlt|ports.ModShift)
197 checkBorderScenes(t, 0, 0, scenes)
198 if got := shown(scenes); len(got["DP-1"]) != map[string]int{"Left": 1, "Right": 0}[sym] || len(got["DP-2"]) != map[string]int{"Left": 0, "Right": 1}[sym] {
199 t.Fatalf("%s: window did not move: %v", sym, got)
200 }
201 }
202}