turbo-editors/turbo-corepublic Fork 0
v1.0.0
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

toolmenus_test.go · 277 lines · 7.8 KBGo Blame HistoryRaw
🛟 Updated. 28d5985 k33g 18h ago1package app
2
3import (
4 "os"
5 "slices"
6 "testing"
7
📦 Turbo Core f3ade8d k33g 11h ago8 "rickub.com/turbo-editors/turbo-core/tools"
9 "rickub.com/turbo-editors/turbo-core/ui"
🛟 Updated. 28d5985 k33g 18h ago10)
11
12// barMenuOf returns the menu with the given plain label, its items filled in.
13func barMenuOf(t *testing.T, a *App, plain string) *ui.Menu {
14 t.Helper()
15
16 for _, menu := range a.menu.Menus() {
17 if ui.PlainLabel(menu.Label) != plain {
18 continue
19 }
20 if menu.OnOpen != nil {
21 menu.OnOpen()
22 }
23 return menu
24 }
25 t.Fatalf("there is no %q menu on the bar: %v", plain, barLabels(a))
26 return nil
27}
28
29// barLabels returns the bar's menus as plain names, for failure messages.
30func barLabels(a *App) []string {
31 var names []string
32 for _, menu := range a.menu.Menus() {
33 names = append(names, ui.PlainLabel(menu.Label))
34 }
35 return names
36}
37
38func TestAToolCanAskForAMenuOfItsOwn(t *testing.T) {
39 a, _ := newToolsApp(t, `
40[[tool]]
41name = "~E~cho"
42command = "echo TADA"
43menu = "Tools"
44`)
45
46 menu := barMenuOf(t, a, "Tools")
47 if len(menu.Items) != 1 || ui.PlainLabel(menu.Items[0].Label) != "Echo" {
48 t.Errorf("the Tools menu holds %v", labels(menu.Items))
49 }
50}
51
52func TestAToolInItsOwnMenuIsNotAlsoInTheToolchainMenu(t *testing.T) {
53 // A tool showing up twice would be worse than one showing up in the wrong
54 // place: nobody would know which line does what.
55 a, _ := newToolsApp(t, `
56[[tool]]
57name = "Echo"
58command = "echo TADA"
59menu = "Tools"
60`)
61
62 for _, item := range barMenuOf(t, a, a.toolsMenuName()).Items {
63 if ui.PlainLabel(item.Label) == "Echo" {
64 t.Errorf("Echo is in the %s menu as well as its own", a.toolsMenuName())
65 }
66 }
67}
68
69func TestAToolThatNamesNoMenuStaysInTheToolchainMenu(t *testing.T) {
70 a, _ := newToolsApp(t, `
71[[tool]]
72name = "Test"
73command = "go test ./..."
74
75[[tool]]
76name = "Echo"
77command = "echo TADA"
78menu = "Tools"
79`)
80
81 names := labels(barMenuOf(t, a, a.toolsMenuName()).Items)
82 if len(names) == 0 || names[0] != "Test" {
83 t.Errorf("the %s menu holds %v, wanted Test first", a.toolsMenuName(), names)
84 }
85}
86
87func TestCreatedMenusFollowTheOrderOfTheFile(t *testing.T) {
88 a, _ := newToolsApp(t, `
89[[tool]]
90name = "Up"
91command = "docker compose up"
92menu = "Docker"
93
94[[tool]]
95name = "Echo"
96command = "echo x"
97menu = "Tools"
98`)
99
100 got := barLabels(a)
101 docker, echoMenu := positionOf(got, "Docker"), positionOf(got, "Tools")
102 switch {
103 case docker < 0 || echoMenu < 0:
104 t.Fatalf("the bar is %v", got)
105 case docker > echoMenu:
106 t.Errorf("Docker comes after Tools on the bar: %v", got)
107 }
108}
109
110func TestCreatedMenusSitBetweenTheToolchainMenuAndHelp(t *testing.T) {
111 // Help is where Turbo C left it, and where a reader looks for it.
112 a, _ := newToolsApp(t, "[[tool]]\nname = \"Echo\"\ncommand = \"echo x\"\nmenu = \"Tools\"\n")
113
114 got := barLabels(a)
115 toolchain := positionOf(got, a.toolsMenuName())
116 if created, help := positionOf(got, "Tools"), positionOf(got, "Help"); !(toolchain < created && created < help) {
117 t.Errorf("the bar is %v", got)
118 }
119}
120
121func TestNoCreatedMenuClashesWithAFixedOne(t *testing.T) {
122 // The bar answers the first match, so a clash makes one of the two menus
123 // unreachable from the keyboard — silently, and with every test passing.
124 a, _ := newToolsApp(t, `
125[[tool]]
126name = "a"
127command = "true"
128menu = "File"
129
130[[tool]]
131name = "b"
132command = "true"
133menu = "Search"
134
135[[tool]]
136name = "c"
137command = "true"
138menu = "Help"
139`)
140
141 seen := map[rune]string{}
142 for _, menu := range a.menu.Menus() {
143 label, hot, _ := ui.SplitHotKey(menu.Label)
144 if hot == 0 {
145 continue // a name with no free letter left; F10 still reaches it
146 }
147 if other, clash := seen[hot]; clash {
148 t.Errorf("%q and %q both answer to Alt-%c", other, label, hot)
149 }
150 seen[hot] = label
151 }
152}
153
154func TestACreatedMenuTakesTheFirstFreeLetterOfItsName(t *testing.T) {
155 a, _ := newToolsApp(t, "[[tool]]\nname = \"a\"\ncommand = \"true\"\nmenu = \"Format\"\n")
156
157 // F is File's, o is Options', r is Run's — m is the first letter free.
158 if label := barMenuOf(t, a, "Format").Label; label != "For~m~at" {
159 t.Errorf("Format got the label %q, want For~m~at", label)
160 }
161}
162
163func TestAMenuNameMayChooseItsOwnHotKey(t *testing.T) {
164 // A free letter written into the name is kept. One that is already taken
165 // is not — TestHotKeyLabelGivesUpAChosenLetterThatIsTaken covers that.
166 a, _ := newToolsApp(t, "[[tool]]\nname = \"a\"\ncommand = \"true\"\nmenu = \"Doc~k~er\"\n")
167
168 if label := barMenuOf(t, a, "Docker").Label; label != "Doc~k~er" {
169 t.Errorf("the chosen hot key was not kept: %q", label)
170 }
171}
172
173func TestHotKeyLabelFallsBackWhenEveryLetterIsTaken(t *testing.T) {
174 taken := map[rune]bool{'a': true, 'b': true}
175
176 // No hot key beats one that opens somebody else's menu.
177 if got := hotKeyLabel("Ab", taken); got != "Ab" {
178 t.Errorf("hotKeyLabel = %q, want the name unmarked", got)
179 }
180}
181
182func TestHotKeyLabelGivesUpAChosenLetterThatIsTaken(t *testing.T) {
183 taken := map[rune]bool{'f': true}
184
185 if got := hotKeyLabel("~F~oo", taken); got != "F~o~o" {
186 t.Errorf("hotKeyLabel = %q, want F~o~o", got)
187 }
188}
189
190func TestHotKeyLabelReservesWhatItHandsOut(t *testing.T) {
191 taken := map[rune]bool{}
192
193 first := hotKeyLabel("Tools", taken)
194 second := hotKeyLabel("Test", taken)
195 if first == second {
196 t.Fatalf("both menus got %q", first)
197 }
198 if second != "T~e~st" {
199 t.Errorf("the second menu got %q, want T~e~st", second)
200 }
201}
202
203func TestTheBarPicksUpAMenuAddedWhileRunning(t *testing.T) {
204 // The set of menus comes from the file, so editing the file has to change
205 // the bar; Menu.OnOpen only refills the items inside one.
206 a, root := newToolsApp(t, "[[tool]]\nname = \"Test\"\ncommand = \"go test\"\n")
207 if positionOf(barLabels(a), "Docker") >= 0 {
208 t.Fatal("there is a Docker menu before the file asks for one")
209 }
210
211 writeTools(t, root, "[[tool]]\nname = \"Up\"\ncommand = \"docker compose up\"\nmenu = \"Docker\"\n")
212 a.tick()
213
214 if positionOf(barLabels(a), "Docker") < 0 {
215 t.Errorf("the bar is still %v", barLabels(a))
216 }
217}
218
219func TestTheBarDropsAMenuTheFileNoLongerAsksFor(t *testing.T) {
220 a, root := newToolsApp(t, "[[tool]]\nname = \"Up\"\ncommand = \"docker compose up\"\nmenu = \"Docker\"\n")
221 if positionOf(barLabels(a), "Docker") < 0 {
222 t.Fatalf("the bar starts as %v", barLabels(a))
223 }
224
225 writeTools(t, root, "[[tool]]\nname = \"Test\"\ncommand = \"go test ./...\"\n")
226 a.tick()
227
228 if positionOf(barLabels(a), "Docker") >= 0 {
229 t.Errorf("the bar is still %v", barLabels(a))
230 }
231}
232
233func TestAnUnchangedToolsFileLeavesTheBarAlone(t *testing.T) {
234 // Rebuilding on every turn of the loop would close a menu under whoever
235 // was reading it, and throw away the OnOpen closures each turn.
236 a, _ := newToolsApp(t, "[[tool]]\nname = \"Echo\"\ncommand = \"echo x\"\nmenu = \"Tools\"\n")
237 before := a.menu.Menus()
238
239 a.tick()
240
241 if after := a.menu.Menus(); &before[0] != &after[0] {
242 t.Error("the bar was rebuilt although the tools file had not changed")
243 }
244}
245
246func TestAnUnreadableToolsFileCreatesNoMenus(t *testing.T) {
247 // The Go menu already says the file cannot be read; saying it again once
248 // per menu the file was about to create would be noise.
249 a, _ := newToolsApp(t, "[[tool]\nname = broken\n")
250
251 // Named rather than counted: a count says nothing about *which* menu
252 // appeared, and this list is also the one place the whole fixed bar is
253 // written down, which a tutorial's "press → five times" depends on.
254 want := []string{"File", "Edit", "Search", "Run", "Code", "Options", "Window", "Snippets", "Agent", "Test", "Help"}
255 if got := barLabels(a); !slices.Equal(got, want) {
256 t.Errorf("the bar is %v, want %v", got, want)
257 }
258}
259
260// writeTools replaces a project's tools file.
261func writeTools(t *testing.T, root, contents string) {
262 t.Helper()
263
264 if err := os.WriteFile(tools.Path(testProfile(), root), []byte(contents), 0o644); err != nil {
265 t.Fatalf("writing the tools file: %v", err)
266 }
267}
268
269// positionOf returns where a name sits in a list, or -1 when it is absent.
270func positionOf(names []string, want string) int {
271 for i, name := range names {
272 if name == want {
273 return i
274 }
275 }
276 return -1
277}