1package core_test
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/neferwl/internal/adapters/config"
8 "github.com/bnema/neferwl/internal/core"
9 "github.com/bnema/neferwl/internal/ports"
10)
11
12func TestWorkspaceChannelCapacity(t *testing.T) {
13 _, err := core.New(config.Defaults(), core.Channels{Scenes: make(chan []ports.Scene, 1), Workspaces: make(chan ports.Workspaces, 2)})
14 if err == nil {
15 t.Fatal("expected capacity-1 validation")
16 }
17}
18
19func TestWorkspaceSnapshotsAndActivation(t *testing.T) {
20 cfg := config.Defaults()
21 cfg.Workspaces = append(cfg.Workspaces, ports.WorkspaceConfig{Name: "dev"})
22 client := make(chan ports.ClientEvent, 16)
23 input := make(chan ports.InputEvent, 8)
24 output := make(chan ports.OutputEvent, 8)
25 commands := make(chan ports.ClientCommand, 32)
26 snapshots := make(chan ports.Workspaces, 1)
27 scenes := make(chan []ports.Scene, 1)
28 state := make(chan ports.State, 1)
29 spawn := make(chan ports.SpawnRequest, 8)
30 c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, State: state, Scenes: scenes, Spawn: spawn})
31 if err != nil {
32 t.Fatal(err)
33 }
34 ctx, cancel := context.WithCancel(context.Background())
35 done := make(chan error, 1)
36 go func() { done <- c.Run(ctx) }()
37 t.Cleanup(func() {
38 cancel()
39 if err := <-done; err != nil {
40 t.Error(err)
41 }
42 })
43 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 100, Height: 80}}
44 first := receive(t, snapshots)
45 for len(first.Outputs) == 0 {
46 first = receive(t, snapshots)
47 }
48 if len(first.Outputs) != 1 || len(first.Outputs[0].Workspaces) < 2 || !first.Outputs[0].Workspaces[0].Active {
49 t.Fatal(first)
50 }
51 hidden := first.Outputs[0].Workspaces[len(first.Outputs[0].Workspaces)-1]
52 if hidden.Configured != "dev" || !hidden.Hidden {
53 t.Fatalf("configured hidden workspace: %+v", hidden)
54 }
55 id := first.Outputs[0].Workspaces[0].ID
56 client <- ports.WindowMapped{ID: 1}
57 added := receive(t, snapshots)
58 if len(added.Outputs[0].Workspaces) < 2 || added.Outputs[0].Workspaces[0].ID != id || !added.Outputs[0].Workspaces[1].Hidden {
59 t.Fatal(added)
60 }
61 spare := added.Outputs[0].Workspaces[1].ID
62 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "B", Width: 100, Height: 80}}
63 two := receive(t, snapshots)
64 if len(two.Outputs) != 2 || two.Outputs[0].Workspaces[0].ID != id {
65 t.Fatal(two)
66 }
67 // Activation on B also focuses B, without disturbing A's active workspace.
68 bID := two.Outputs[1].Workspaces[0].ID
69 client <- ports.WorkspaceActivate{IDs: []uint64{bID}}
70 for receive(t, state).Output != "B" {
71 }
72 // Both requests are applied in order in one event: A wins final focus.
73 client <- ports.WorkspaceActivate{IDs: []uint64{bID, spare}}
74 for receive(t, state).Output != "A" {
75 }
76 active := receive(t, snapshots)
77 if !active.Outputs[0].Workspaces[1].Active || active.Outputs[1].Workspaces[0].Active != true {
78 t.Fatal(active)
79 }
80 client <- ports.WorkspaceActivate{IDs: []uint64{999999999}}
81 scene(t, scenes)
82 select {
83 case v := <-snapshots:
84 t.Fatalf("unknown ID changed snapshot: %v", v)
85 default:
86 }
87 // Move the populated workspace right; its ID follows it.
88 client <- ports.WorkspaceActivate{IDs: []uint64{id}}
89 receive(t, snapshots)
90 input <- ports.KeyEvent{Keysym: "Right", Mods: ports.ModSuper | ports.ModCtrl | ports.ModShift, Pressed: true}
91 move := receive(t, snapshots)
92 found := false
93 for _, w := range move.Outputs[1].Workspaces {
94 if w.ID == id {
95 found = true
96 }
97 }
98 if !found {
99 t.Fatalf("workspace ID lost on move: %+v", move)
100 }
101 output <- ports.OutputRemoved{Name: "B"}
102 moved := receive(t, snapshots)
103 if len(moved.Outputs) != 1 || moved.Outputs[0].Name != "A" {
104 t.Fatal(moved)
105 }
106}
107
108func TestMoveWorkspaceReordersSnapshot(t *testing.T) {
109 cfg := config.Defaults()
110 client := make(chan ports.ClientEvent, 16)
111 input := make(chan ports.InputEvent, 8)
112 output := make(chan ports.OutputEvent, 8)
113 snapshots := make(chan ports.Workspaces, 1)
114 scenes := make(chan []ports.Scene, 1)
115 spawn := make(chan ports.SpawnRequest, 8)
116 commands := make(chan ports.ClientCommand, 32)
117 c, err := core.New(cfg, core.Channels{Client: client, Input: input, Output: output, Commands: commands, Workspaces: snapshots, Scenes: scenes, Spawn: spawn})
118 if err != nil {
119 t.Fatal(err)
120 }
121 ctx, cancel := context.WithCancel(context.Background())
122 done := make(chan error, 1)
123 go func() { done <- c.Run(ctx) }()
124 go func() {
125 for {
126 select {
127 case <-scenes:
128 case <-commands:
129 case <-spawn:
130 case <-ctx.Done():
131 return
132 }
133 }
134 }()
135 t.Cleanup(func() {
136 cancel()
137 if err := <-done; err != nil {
138 t.Error(err)
139 }
140 })
141 output <- ports.OutputAdded{Info: ports.OutputInfo{Name: "A", Width: 100, Height: 80}}
142 until := func(ok func(ws []ports.WorkspaceInfo) bool) []ports.WorkspaceInfo {
143 t.Helper()
144 for {
145 v := receive(t, snapshots)
146 if len(v.Outputs) > 0 && ok(v.Outputs[0].Workspaces) {
147 return v.Outputs[0].Workspaces
148 }
149 }
150 }
151 client <- ports.WindowMapped{ID: 1}
152 until(func(ws []ports.WorkspaceInfo) bool { return len(ws) >= 2 })
153 input <- ports.KeyEvent{Keysym: "Next", Mods: ports.ModSuper, Pressed: true}
154 until(func(ws []ports.WorkspaceInfo) bool { return ws[1].Active })
155 client <- ports.WindowMapped{ID: 2}
156 ws := until(func(ws []ports.WorkspaceInfo) bool { return len(ws) >= 3 && ws[1].Active })
157 before := ports.Workspaces{Outputs: []ports.WorkspaceOutput{{Workspaces: ws}}}
158 first, second := before.Outputs[0].Workspaces[0].ID, before.Outputs[0].Workspaces[1].ID
159 input <- ports.KeyEvent{Keysym: "Up", Mods: ports.ModSuper | ports.ModCtrl | ports.ModShift, Pressed: true}
160 ws = until(func(ws []ports.WorkspaceInfo) bool { return ws[0].Active })
161 if ws[0].ID != second || ws[1].ID != first || !ws[0].Active || ws[0].Index != 0 || ws[1].Index != 1 {
162 t.Fatalf("%+v", ws)
163 }
164}