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