1package core
2
3import (
4 "testing"
5
6 "github.com/bnema/neferwl/internal/ports"
7)
8
9type tile struct {
10 name string
11 x, y, w, h int
12}
13
14// layoutCore builds a core whose screens sit at the given logical rectangles,
15// in order, with the screen at index focus focused.
16func layoutCore(focus int, tiles ...tile) *Core {
17 c := &Core{focusScreen: focus}
18 for _, t := range tiles {
19 s := &screen{info: ports.OutputInfo{Name: t.name}, mon: newMonitor(t.name, ""), x: t.x, y: t.y}
20 s.mon.SetOutput(t.w, t.h)
21 c.screens = append(c.screens, s)
22 }
23 return c
24}
25
26func TestNeighbor(t *testing.T) {
27 dirs := []direction{dirLeft, dirRight, dirUp, dirDown}
28 dirNames := [...]string{"left", "right", "up", "down"}
29 // want holds the expected neighbor name for left, right, up, down; "" is
30 // the edge.
31 type step struct {
32 from string
33 want [4]string
34 }
35 cases := []struct {
36 name string
37 tiles []tile
38 steps []step
39 }{
40 {"row of three in connection order", []tile{{"B", 200, 0, 200, 100}, {"A", 0, 0, 200, 100}, {"C", 400, 0, 200, 100}}, []step{
41 {"A", [4]string{"", "B", "", ""}},
42 {"B", [4]string{"A", "C", "", ""}},
43 {"C", [4]string{"B", "", "", ""}},
44 }},
45 {"column of two", []tile{{"A", 0, 0, 200, 100}, {"B", 0, 100, 200, 100}}, []step{
46 {"A", [4]string{"", "", "", "B"}},
47 {"B", [4]string{"", "", "A", ""}},
48 }},
49 {"row with y offsets", []tile{{"A", 0, 0, 200, 100}, {"B", 200, 60, 200, 100}, {"C", 400, 120, 200, 100}}, []step{
50 // A and C share no edge range: only diagonal vertical neighbors.
51 {"A", [4]string{"", "B", "", "C"}},
52 {"B", [4]string{"A", "C", "", ""}},
53 {"C", [4]string{"B", "", "A", ""}},
54 }},
55 {"diagonal only", []tile{{"A", 0, 0, 100, 100}, {"B", 100, 150, 100, 100}}, []step{
56 {"A", [4]string{"", "B", "", "B"}},
57 {"B", [4]string{"A", "", "A", ""}},
58 }},
59 {"shared edge beats nearer diagonal", []tile{{"A", 0, 0, 100, 100}, {"B", 100, 150, 100, 100}, {"C", 300, 50, 100, 100}}, []step{
60 {"A", [4]string{"", "C", "", "B"}},
61 }},
62 {"smaller gap wins", []tile{{"A", 0, 0, 100, 100}, {"far", 300, 0, 100, 100}, {"near", 150, 0, 100, 100}}, []step{
63 {"A", [4]string{"", "near", "", ""}},
64 }},
65 {"larger shared edge wins at equal gap", []tile{{"A", 0, 0, 100, 100}, {"little", 100, 80, 100, 100}, {"big", 100, 20, 100, 60}}, []step{
66 {"A", [4]string{"", "big", "", ""}},
67 }},
68 {"closer center wins at equal overlap", []tile{{"A", 0, 0, 100, 100}, {"low", 100, 50, 100, 100}, {"mid", 100, -20, 100, 70}}, []step{
69 // Both share 50 rows; mid's center is nearer to A's.
70 {"A", [4]string{"", "mid", "", ""}},
71 }},
72 {"lowest index at a full tie", []tile{{"A", 0, 0, 100, 100}, {"first", 100, 0, 100, 100}, {"second", 100, 0, 100, 100}}, []step{
73 {"A", [4]string{"", "first", "", ""}},
74 }},
75 {"overlapping screens fall back to centers", []tile{{"A", 0, 0, 200, 100}, {"B", 100, 0, 200, 100}}, []step{
76 // Centers are 100 and 200 apart on x only.
77 {"A", [4]string{"", "B", "", ""}},
78 {"B", [4]string{"A", "", "", ""}},
79 }},
80 {"outputs anchored to the same reference", []tile{{"R", 0, 0, 100, 100}, {"X", 100, 0, 100, 100}, {"Y", 100, 0, 300, 50}}, []step{
81 // X and Y overlap; the strict pass finds nothing between them.
82 {"X", [4]string{"R", "Y", "Y", ""}},
83 // R is fully left of Y and wins over the closer, overlapping X.
84 {"Y", [4]string{"R", "", "", "X"}},
85 }},
86 {"same-height outputs anchored to the same reference: one is unreachable by direction", []tile{{"R", 0, 0, 100, 100}, {"Y", 100, 0, 300, 100}, {"X", 100, 0, 100, 100}}, []step{
87 {"R", [4]string{"", "Y", "", ""}},
88 {"Y", [4]string{"R", "", "", ""}},
89 {"X", [4]string{"R", "Y", "", ""}},
90 }},
91 {"strict candidate beats a closer overlapping one", []tile{{"A", 0, 0, 100, 100}, {"near", 50, 0, 100, 100}, {"far", 300, 0, 100, 100}}, []step{
92 {"A", [4]string{"", "far", "", ""}},
93 {"near", [4]string{"A", "far", "", ""}},
94 }},
95 {"identical centers are unreachable", []tile{{"A", 0, 0, 100, 100}, {"B", 0, 0, 100, 100}}, []step{
96 {"A", [4]string{"", "", "", ""}},
97 {"B", [4]string{"", "", "", ""}},
98 }},
99 {"placeholder ignored", []tile{{"A", 0, 0, 100, 100}, {"", 100, 0, 100, 100}, {"B", 300, 0, 100, 100}}, []step{
100 {"A", [4]string{"", "B", "", ""}},
101 {"B", [4]string{"A", "", "", ""}},
102 }},
103 {"two by three grid", []tile{
104 {"DP-1", 0, 0, 2560, 1440}, {"DP-2", -1920, 360, 1920, 1080}, {"DP-3", 2560, 360, 1920, 1080},
105 {"HDMI-1", 0, -1440, 2560, 1440}, {"DP-4", -1920, -1200, 1920, 1200}, {"DP-5", 2560, -1200, 1920, 1200},
106 }, []step{
107 {"DP-4", [4]string{"", "HDMI-1", "", "DP-2"}},
108 {"HDMI-1", [4]string{"DP-4", "DP-5", "", "DP-1"}},
109 {"DP-5", [4]string{"HDMI-1", "", "", "DP-3"}},
110 {"DP-2", [4]string{"", "DP-1", "DP-4", ""}},
111 {"DP-1", [4]string{"DP-2", "DP-3", "HDMI-1", ""}},
112 {"DP-3", [4]string{"DP-1", "", "DP-5", ""}},
113 }},
114 }
115 for _, tc := range cases {
116 t.Run(tc.name, func(t *testing.T) {
117 for _, st := range tc.steps {
118 c := layoutCore(0, tc.tiles...)
119 for i, s := range c.screens {
120 if s.name() == st.from {
121 c.focusScreen = i
122 }
123 }
124 for i, d := range dirs {
125 got := ""
126 if n := c.neighbor(d); n >= 0 {
127 got = c.screens[n].name()
128 }
129 if got != st.want[i] {
130 t.Errorf("from %s %s: got %q, want %q", st.from, dirNames[i], got, st.want[i])
131 }
132 }
133 }
134 })
135 }
136 if n := layoutCore(0, tile{"A", 0, 0, 10, 10}).neighbor(direction(0)); n != -1 {
137 t.Fatalf("invalid direction: %d", n)
138 }
139}