1package command
2
3import (
4 "errors"
5 "regexp"
6 "strconv"
7 "testing"
8)
9
10func TestRegistryCodesAndSlugsAreUniqueInOrder(t *testing.T) {
11 commands := PaletteRegistry()
12 wantCodes := []string{"CNT", "CNS", "CES", "CLT", "SPR", "SPL", "SPU", "SPD", "MPL", "MPR", "STP", "TFS", "TFP", "CFP", "MFP", "MAT", "FPL", "FPR", "FPU", "FPD", "RSZ", "GPW", "SPW", "GPH", "SPH", "EQP", "NXT", "PVT", "BCK", "JRS", "SSP", "NTC", "YLN", "VIS", "RNS", "RNT", "DET"}
13
14 if len(commands) != len(wantCodes) {
15 t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes))
16 }
17
18 codePattern := regexp.MustCompile(`^[A-Z]{3}$`)
19 seenCodes := make(map[string]bool, len(commands))
20 seenSlugs := make(map[string]bool, len(commands))
21 for i, cmd := range commands {
22 if cmd.Code != wantCodes[i] {
23 t.Fatalf("Registry()[%d].Code = %q, want %q", i, cmd.Code, wantCodes[i])
24 }
25 if !codePattern.MatchString(cmd.Code) {
26 t.Errorf("Registry()[%d].Code = %q, want three uppercase letters", i, cmd.Code)
27 }
28 if seenCodes[cmd.Code] {
29 t.Errorf("Registry()[%d].Code = %q, duplicate code", i, cmd.Code)
30 }
31 seenCodes[cmd.Code] = true
32 if cmd.Slug == "" {
33 t.Errorf("Registry()[%d].Slug is empty", i)
34 }
35 if seenSlugs[cmd.Slug] {
36 t.Errorf("Registry()[%d].Slug = %q, duplicate slug", i, cmd.Slug)
37 }
38 seenSlugs[cmd.Slug] = true
39 }
40}
41
42func TestMoveCommandsExposeExactRegistryContracts(t *testing.T) {
43 tests := []struct {
44 code, slug, name, desc, usage string
45 target TargetKind
46 args []string
47 wantCall string
48 }{
49 {
50 code: "MFP", slug: "move-pane", name: "Move pane to tab",
51 desc: "Move focused pane to tab", usage: "move-pane <destination-session> <destination-tab-id>",
52 target: TargetPane, args: []string{"work", "t_dest"}, wantCall: "move-pane:work:t_dest",
53 },
54 {
55 code: "MAT", slug: "move-tab", name: "Move tab to session",
56 desc: "Move active tab to session", usage: "move-tab <destination-session>",
57 target: TargetTab, args: []string{"work"}, wantCall: "move-tab:work",
58 },
59 }
60 for _, tt := range tests {
61 t.Run(tt.slug, func(t *testing.T) {
62 cmd, ok := BySlug(tt.slug)
63 if !ok {
64 t.Fatalf("BySlug(%q) missing", tt.slug)
65 }
66 if cmd.Code != tt.code || cmd.Name != tt.name || cmd.Desc != tt.desc || cmd.Usage != tt.usage {
67 t.Fatalf("command metadata = %#v", cmd)
68 }
69 if !cmd.PaletteVisible || !cmd.Scriptable || cmd.Target != tt.target || cmd.Scope != CommandScopeCrossSession {
70 t.Fatalf("command visibility/target/scope = palette:%v scriptable:%v target:%v scope:%v", cmd.PaletteVisible, cmd.Scriptable, cmd.Target, cmd.Scope)
71 }
72 ctx := &controlSpy{}
73 if _, err := cmd.Control(ctx, tt.args, ControlOptions{}); err != nil {
74 t.Fatalf("Control(%v): %v", tt.args, err)
75 }
76 if ctx.call != tt.wantCall {
77 t.Fatalf("Control(%v) call = %q, want %q", tt.args, ctx.call, tt.wantCall)
78 }
79 })
80 }
81}
82
83func TestMoveControlHandlersRequireExactArguments(t *testing.T) {
84 for _, tt := range []struct {
85 slug string
86 args []string
87 }{
88 {slug: "move-pane"},
89 {slug: "move-pane", args: []string{"work"}},
90 {slug: "move-pane", args: []string{"work", "t_dest", "extra"}},
91 {slug: "move-pane", args: []string{"", "t_dest"}},
92 {slug: "move-tab"},
93 {slug: "move-tab", args: []string{"work", "extra"}},
94 {slug: "move-tab", args: []string{""}},
95 } {
96 t.Run(tt.slug+"/"+strconv.Itoa(len(tt.args)), func(t *testing.T) {
97 cmd, ok := BySlug(tt.slug)
98 if !ok {
99 t.Fatalf("BySlug(%q) missing", tt.slug)
100 }
101 ctx := &controlSpy{}
102 if _, err := cmd.Control(ctx, tt.args, ControlOptions{}); !errors.Is(err, ErrInvalidArguments) {
103 t.Fatalf("Control(%v) error = %v, want ErrInvalidArguments", tt.args, err)
104 }
105 if ctx.call != "" {
106 t.Fatalf("invalid arguments delegated: %q", ctx.call)
107 }
108 })
109 }
110}
111
112func TestBySlugExactMatch(t *testing.T) {
113 tests := []struct {
114 slug string
115 ok bool
116 }{
117 {slug: "split-right", ok: true},
118 {slug: "consume-or-expel-pane-left", ok: true},
119 {slug: "consume-or-expel-pane-right", ok: true},
120 {slug: "SPLIT-RIGHT", ok: false},
121 {slug: "toast", ok: true},
122 {slug: "list-panes", ok: true},
123 {slug: "does-not-exist", ok: false},
124 }
125 for _, tt := range tests {
126 t.Run(tt.slug, func(t *testing.T) {
127 if _, ok := BySlug(tt.slug); ok != tt.ok {
128 t.Fatalf("BySlug(%q) ok = %v, want %v", tt.slug, ok, tt.ok)
129 }
130 })
131 }
132}
133
134func TestRegistryControlMetadata(t *testing.T) {
135 scriptable := map[string]TargetKind{
136 "split-right": TargetPane, "split-left": TargetPane, "split-up": TargetPane,
137 "split-down": TargetPane, "consume-or-expel-pane-left": TargetPane,
138 "consume-or-expel-pane-right": TargetPane, "stack-pane": TargetPane, "toggle-stack": TargetPane,
139 "close-pane": TargetPane, "focus-pane-left": TargetPane, "focus-pane-right": TargetPane,
140 "focus-pane-up": TargetPane, "focus-pane-down": TargetPane,
141 "new-tab": TargetSession, "close-tab": TargetTab, "next-tab": TargetSession,
142 "previous-tab": TargetSession, "rename-session": TargetSession, "rename-tab": TargetTab,
143 "new-session": TargetSession, "toast": TargetSession,
144 "grow-pane-width": TargetPane, "shrink-pane-width": TargetPane,
145 "grow-pane-height": TargetPane, "shrink-pane-height": TargetPane, "equalize-panes": TargetTab,
146 "move-pane": TargetPane, "move-tab": TargetTab,
147 "list-sessions": TargetNone, "list-tabs": TargetSession, "list-panes": TargetTab,
148 "remote-catalog": TargetNone, "env": TargetSession,
149 }
150 notScriptable := []string{
151 "session-picker", "notifications", "visual-mode", "yank-last-notification",
152 "detach", "back-session", "jump-recent-session", "toggle-floating-pane", "resize-pane",
153 }
154 for slug, target := range scriptable {
155 cmd, ok := BySlug(slug)
156 if !ok || !cmd.Scriptable {
157 t.Errorf("%s: want scriptable entry, ok=%v scriptable=%v", slug, ok, cmd.Scriptable)
158 continue
159 }
160 if cmd.Target != target || cmd.Control == nil || cmd.Usage == "" || cmd.Desc == "" {
161 t.Errorf("%s: incomplete control metadata: %#v", slug, cmd)
162 }
163 }
164 for _, slug := range notScriptable {
165 cmd, ok := BySlug(slug)
166 if !ok || cmd.Scriptable || cmd.Control != nil {
167 t.Errorf("%s: want present and non-scriptable, got %#v, ok=%v", slug, cmd, ok)
168 }
169 }
170}
171
172func TestPaletteRegistryHidesAPIOnly(t *testing.T) {
173 for _, cmd := range PaletteRegistry() {
174 if !cmd.PaletteVisible || cmd.Run == nil {
175 t.Errorf("palette command is not executable and visible: %#v", cmd)
176 }
177 if cmd.Slug == "toast" || cmd.Slug == "list-sessions" || cmd.Slug == "list-tabs" || cmd.Slug == "list-panes" || cmd.Slug == "remote-catalog" || cmd.Slug == "env" {
178 t.Errorf("API-only command %q is visible", cmd.Slug)
179 }
180 }
181}
182
183func TestSessionRecoveryCommand(t *testing.T) {
184 cmd, ok := BySlug("session-recovery")
185 if !ok || !cmd.Scriptable || cmd.Target != TargetNone {
186 t.Fatalf("session-recovery command = %#v, ok=%v", cmd, ok)
187 }
188 tests := []struct {
189 args []string
190 wantCall string
191 wantErr error
192 }{
193 {args: []string{"discard"}, wantCall: "session-recovery:discard"},
194 {args: []string{"retry"}, wantErr: ErrInvalidArguments},
195 {args: []string{"restore", "7"}, wantErr: ErrInvalidArguments},
196 {args: []string{"export", "/tmp/export"}, wantErr: ErrInvalidArguments},
197 {args: nil, wantErr: ErrInvalidArguments},
198 }
199 for _, test := range tests {
200 ctx := &controlSpy{recoveryOutput: "recovery output"}
201 got, err := cmd.Control(ctx, test.args, ControlOptions{})
202 wantOutput := ""
203 if test.wantCall != "" {
204 wantOutput = ctx.recoveryOutput
205 }
206 if !errors.Is(err, test.wantErr) || ctx.call != test.wantCall || got.Output != wantOutput {
207 t.Errorf("args %v: call/output/error = %q/%q/%v, want %q/%q/%v", test.args, ctx.call, got.Output, err, test.wantCall, wantOutput, test.wantErr)
208 }
209 }
210}
211
212func TestRemoteCatalogCommand(t *testing.T) {
213 cmd, ok := BySlug("remote-catalog")
214 if !ok {
215 t.Fatal("BySlug(\"remote-catalog\") missing")
216 }
217 if cmd.Name != "Remote catalog" || cmd.Desc == "" || cmd.Usage != "remote-catalog --json" {
218 t.Fatalf("remote-catalog metadata = %#v", cmd)
219 }
220 if !cmd.Scriptable || cmd.PaletteVisible || cmd.Target != TargetNone || cmd.Run != nil || cmd.Control == nil {
221 t.Fatalf("remote-catalog visibility/target = %#v", cmd)
222 }
223
224 tests := []struct {
225 name string
226 args []string
227 opts ControlOptions
228 wantCall string
229 wantErr error
230 }{
231 {name: "json delegates", opts: ControlOptions{JSON: true}, wantCall: "remote-catalog:true"},
232 {name: "rejects missing json", wantErr: ErrInvalidArguments},
233 {name: "rejects args", args: []string{"extra"}, opts: ControlOptions{JSON: true}, wantErr: ErrInvalidArguments},
234 {name: "rejects args without json", args: []string{"extra"}, wantErr: ErrInvalidArguments},
235 }
236 for _, tt := range tests {
237 t.Run(tt.name, func(t *testing.T) {
238 ctx := &controlSpy{catalogOutput: `{"protocol_version":19,"sessions":[]}` + "\n"}
239 got, err := cmd.Control(ctx, tt.args, tt.opts)
240 wantOutput := ""
241 if tt.wantCall != "" {
242 wantOutput = ctx.catalogOutput
243 }
244 if !errors.Is(err, tt.wantErr) || ctx.call != tt.wantCall || got.Output != wantOutput {
245 t.Fatalf("Control() call/output/error = %q/%q/%v, want %q/%q/%v", ctx.call, got.Output, err, tt.wantCall, wantOutput, tt.wantErr)
246 }
247 })
248 }
249}
250
251func TestControlHandlersValidateAndDelegate(t *testing.T) {
252 tests := []struct {
253 name string
254 slug string
255 args []string
256 opts ControlOptions
257 wantCall string
258 wantOutput string
259 wantErr error
260 }{
261 {name: "zero argument mutation delegates", slug: "split-right", wantCall: "split-right"},
262 {name: "zero argument mutation rejects args", slug: "split-right", args: []string{"extra"}, wantErr: ErrInvalidArguments},
263 {name: "consume or expel left delegates", slug: "consume-or-expel-pane-left", wantCall: "consume-or-expel-pane-left"},
264 {name: "consume or expel right delegates", slug: "consume-or-expel-pane-right", wantCall: "consume-or-expel-pane-right"},
265 {name: "grow width delegates", slug: "grow-pane-width", wantCall: "grow-pane-width"},
266 {name: "shrink width delegates", slug: "shrink-pane-width", wantCall: "shrink-pane-width"},
267 {name: "grow height delegates", slug: "grow-pane-height", wantCall: "grow-pane-height"},
268 {name: "shrink height delegates", slug: "shrink-pane-height", wantCall: "shrink-pane-height"},
269 {name: "equalize delegates", slug: "equalize-panes", wantCall: "equalize-panes"},
270 {name: "resize mutation rejects arguments", slug: "grow-pane-width", args: []string{"extra"}, wantErr: ErrInvalidArguments},
271 {name: "equalize rejects arguments", slug: "equalize-panes", args: []string{"extra"}, wantErr: ErrInvalidArguments},
272 {name: "rename delegates name", slug: "rename-tab", args: []string{"editor"}, wantCall: "rename-tab:editor"},
273 {name: "rename rejects empty name", slug: "rename-tab", args: []string{""}, wantErr: ErrInvalidArguments},
274 {name: "toast delegates severity", slug: "toast", args: []string{"-l", "warn", "hello"}, wantCall: "toast:warn:hello"},
275 {name: "toast rejects malformed flags", slug: "toast", args: []string{"-l", "warn"}, wantErr: ErrInvalidArguments},
276 {name: "query delegates JSON option", slug: "list-panes", opts: ControlOptions{JSON: true}, wantCall: "list-panes:true", wantOutput: "panes"},
277 {name: "query rejects args", slug: "list-panes", args: []string{"extra"}, wantErr: ErrInvalidArguments},
278 {name: "env delegates JSON option", slug: "env", opts: ControlOptions{JSON: true}, wantCall: "env:true", wantOutput: "env"},
279 {name: "env rejects args", slug: "env", args: []string{"extra"}, wantErr: ErrInvalidArguments},
280 }
281 for _, tt := range tests {
282 t.Run(tt.name, func(t *testing.T) {
283 cmd, ok := BySlug(tt.slug)
284 if !ok {
285 t.Fatalf("BySlug(%q) missing", tt.slug)
286 }
287 ctx := &controlSpy{}
288 got, err := cmd.Control(ctx, tt.args, tt.opts)
289 if !errors.Is(err, tt.wantErr) {
290 t.Fatalf("Control() error = %v, want %v", err, tt.wantErr)
291 }
292 if ctx.call != tt.wantCall || got.Output != tt.wantOutput {
293 t.Fatalf("Control() call/output = %q/%q, want %q/%q", ctx.call, got.Output, tt.wantCall, tt.wantOutput)
294 }
295 })
296 }
297}
298
299type controlSpy struct {
300 call string
301 recoveryOutput string
302 catalogOutput string
303}
304
305func (s *controlSpy) record(call string) error { s.call = call; return nil }
306func (s *controlSpy) CreateTab() error { return s.record("new-tab") }
307func (s *controlSpy) CreateSessionNamed(v string) error { return s.record("new-session:" + v) }
308func (s *controlSpy) CloseTab() error { return s.record("close-tab") }
309func (s *controlSpy) ClosePane() error { return s.record("close-pane") }
310func (s *controlSpy) MovePane(session, tab string) error {
311 return s.record("move-pane:" + session + ":" + tab)
312}
313func (s *controlSpy) MoveTab(session string) error { return s.record("move-tab:" + session) }
314func (s *controlSpy) SplitRight() error { return s.record("split-right") }
315func (s *controlSpy) SplitLeft() error { return s.record("split-left") }
316func (s *controlSpy) SplitUp() error { return s.record("split-up") }
317func (s *controlSpy) SplitDown() error { return s.record("split-down") }
318func (s *controlSpy) ConsumeOrExpelPaneLeft() error { return s.record("consume-or-expel-pane-left") }
319func (s *controlSpy) ConsumeOrExpelPaneRight() error { return s.record("consume-or-expel-pane-right") }
320func (s *controlSpy) StackPane() error { return s.record("stack-pane") }
321func (s *controlSpy) ToggleStack() error { return s.record("toggle-stack") }
322func (s *controlSpy) GrowPaneWidth() error { return s.record("grow-pane-width") }
323func (s *controlSpy) ShrinkPaneWidth() error { return s.record("shrink-pane-width") }
324func (s *controlSpy) GrowPaneHeight() error { return s.record("grow-pane-height") }
325func (s *controlSpy) ShrinkPaneHeight() error { return s.record("shrink-pane-height") }
326func (s *controlSpy) EqualizePanes() error { return s.record("equalize-panes") }
327func (s *controlSpy) FocusPaneLeft() error { return s.record("focus-pane-left") }
328func (s *controlSpy) FocusPaneRight() error { return s.record("focus-pane-right") }
329func (s *controlSpy) FocusPaneUp() error { return s.record("focus-pane-up") }
330func (s *controlSpy) FocusPaneDown() error { return s.record("focus-pane-down") }
331func (s *controlSpy) NextTab() error { return s.record("next-tab") }
332func (s *controlSpy) PrevTab() error { return s.record("previous-tab") }
333func (s *controlSpy) RenameSessionTo(v string) error { return s.record("rename-session:" + v) }
334func (s *controlSpy) RenameTabTo(v string) error { return s.record("rename-tab:" + v) }
335func (s *controlSpy) Toast(level, message string) error {
336 return s.record("toast:" + level + ":" + message)
337}
338func (s *controlSpy) SessionRecovery(action string) (string, error) {
339 return s.recoveryOutput, s.record("session-recovery:" + action)
340}
341func (s *controlSpy) ListSessions(json bool) (string, error) {
342 _ = s.record("list-sessions:" + strconv.FormatBool(json))
343 return "sessions", nil
344}
345func (s *controlSpy) ListTabs(json bool) (string, error) {
346 _ = s.record("list-tabs:" + strconv.FormatBool(json))
347 return "tabs", nil
348}
349func (s *controlSpy) ListPanes(json bool) (string, error) {
350 _ = s.record("list-panes:" + strconv.FormatBool(json))
351 return "panes", nil
352}
353func (s *controlSpy) SessionEnvironment(json bool) (string, error) {
354 _ = s.record("env:" + strconv.FormatBool(json))
355 return "env", nil
356}
357func (s *controlSpy) RemoteCatalog(json bool) (string, error) {
358 _ = s.record("remote-catalog:" + strconv.FormatBool(json))
359 return s.catalogOutput, nil
360}
361
362func TestRegistryIncludesFloatingPaneToggle(t *testing.T) {
363 cmd, ok := ByCode("TFP")
364 if !ok {
365 t.Fatal("ByCode(\"TFP\") ok = false, want true")
366 }
367 if cmd.Slug != "toggle-floating-pane" || cmd.Code != "TFP" || cmd.Name != "Toggle floating pane" {
368 t.Fatalf("floating command = %#v, want toggle-floating-pane/TFP/Toggle floating pane", cmd)
369 }
370}
371
372func TestRegistryIncludesNotificationCommands(t *testing.T) {
373 for _, tt := range []struct {
374 code string
375 slug string
376 name string
377 }{
378 {code: "NTC", slug: "notifications", name: "Notifications"},
379 {code: "YLN", slug: "yank-last-notification", name: "Yank last notification"},
380 } {
381 t.Run(tt.code, func(t *testing.T) {
382 cmd, ok := ByCode(tt.code)
383 if !ok {
384 t.Fatalf("ByCode(%q) ok = false, want true", tt.code)
385 }
386 if cmd.Slug != tt.slug || cmd.Code != tt.code || cmd.Name != tt.name {
387 t.Fatalf("command = %#v, want %s/%s/%s", cmd, tt.slug, tt.code, tt.name)
388 }
389 })
390 }
391}
392
393func TestByCodeIsCaseInsensitive(t *testing.T) {
394 for _, code := range []string{"CNT", "cnt", "CnT"} {
395 cmd, ok := ByCode(code)
396 if !ok {
397 t.Fatalf("ByCode(%q) ok = false, want true", code)
398 }
399 if cmd.Code != "CNT" {
400 t.Fatalf("ByCode(%q).Code = %q, want CNT", code, cmd.Code)
401 }
402 }
403
404 if _, ok := ByCode("missing"); ok {
405 t.Fatal("ByCode(unknown) ok = true, want false")
406 }
407}
408
409func TestCommandRunCallsMatchingContextMethod(t *testing.T) {
410 tests := []struct {
411 code string
412 expect func(*MockContext)
413 }{
414 {code: "CNT", expect: func(ctx *MockContext) { ctx.EXPECT().CreateTab().Return(nil).Once() }},
415 {code: "CNS", expect: func(ctx *MockContext) { ctx.EXPECT().CreateSession().Return(nil).Once() }},
416 {code: "CLT", expect: func(ctx *MockContext) { ctx.EXPECT().CloseTab().Return(nil).Once() }},
417 {code: "SPR", expect: func(ctx *MockContext) { ctx.EXPECT().SplitRight().Return(nil).Once() }},
418 {code: "SPL", expect: func(ctx *MockContext) { ctx.EXPECT().SplitLeft().Return(nil).Once() }},
419 {code: "SPU", expect: func(ctx *MockContext) { ctx.EXPECT().SplitUp().Return(nil).Once() }},
420 {code: "SPD", expect: func(ctx *MockContext) { ctx.EXPECT().SplitDown().Return(nil).Once() }},
421 {code: "MPL", expect: func(ctx *MockContext) { ctx.EXPECT().ConsumeOrExpelPaneLeft().Return(nil).Once() }},
422 {code: "MPR", expect: func(ctx *MockContext) { ctx.EXPECT().ConsumeOrExpelPaneRight().Return(nil).Once() }},
423 {code: "STP", expect: func(ctx *MockContext) { ctx.EXPECT().StackPane().Return(nil).Once() }},
424 {code: "TFS", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleStack().Return(nil).Once() }},
425 {code: "TFP", expect: func(ctx *MockContext) { ctx.EXPECT().ToggleFloatingPane().Return(nil).Once() }},
426 {code: "CFP", expect: func(ctx *MockContext) { ctx.EXPECT().ClosePane().Return(nil).Once() }},
427 {code: "MFP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenMovePanePicker().Return(nil).Once() }},
428 {code: "MAT", expect: func(ctx *MockContext) { ctx.EXPECT().OpenMoveTabPicker().Return(nil).Once() }},
429 {code: "FPL", expect: func(ctx *MockContext) { ctx.EXPECT().FocusPaneLeft().Return(nil).Once() }},
430 {code: "FPR", expect: func(ctx *MockContext) { ctx.EXPECT().FocusPaneRight().Return(nil).Once() }},
431 {code: "FPU", expect: func(ctx *MockContext) { ctx.EXPECT().FocusPaneUp().Return(nil).Once() }},
432 {code: "FPD", expect: func(ctx *MockContext) { ctx.EXPECT().FocusPaneDown().Return(nil).Once() }},
433 {code: "RSZ", expect: func(ctx *MockContext) { ctx.EXPECT().EnterResizeMode().Return(nil).Once() }},
434 {code: "GPW", expect: func(ctx *MockContext) { ctx.EXPECT().GrowPaneWidth().Return(nil).Once() }},
435 {code: "SPW", expect: func(ctx *MockContext) { ctx.EXPECT().ShrinkPaneWidth().Return(nil).Once() }},
436 {code: "GPH", expect: func(ctx *MockContext) { ctx.EXPECT().GrowPaneHeight().Return(nil).Once() }},
437 {code: "SPH", expect: func(ctx *MockContext) { ctx.EXPECT().ShrinkPaneHeight().Return(nil).Once() }},
438 {code: "EQP", expect: func(ctx *MockContext) { ctx.EXPECT().EqualizePanes().Return(nil).Once() }},
439 {code: "NXT", expect: func(ctx *MockContext) { ctx.EXPECT().NextTab().Return(nil).Once() }},
440 {code: "PVT", expect: func(ctx *MockContext) { ctx.EXPECT().PrevTab().Return(nil).Once() }},
441 {code: "BCK", expect: func(ctx *MockContext) { ctx.EXPECT().BackSession().Return(nil).Once() }},
442 {code: "JRS", expect: func(ctx *MockContext) { ctx.EXPECT().JumpRecentSession(1).Return(nil).Once() }},
443 {code: "SSP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenSessionPicker().Return(nil).Once() }},
444 {code: "NTC", expect: func(ctx *MockContext) { ctx.EXPECT().OpenNotifications().Return(nil).Once() }},
445 {code: "YLN", expect: func(ctx *MockContext) { ctx.EXPECT().YankLastNotification().Return(nil).Once() }},
446 {code: "VIS", expect: func(ctx *MockContext) { ctx.EXPECT().EnterVisualMode().Return(nil).Once() }},
447 {code: "RNS", expect: func(ctx *MockContext) { ctx.EXPECT().RenameSession().Return(nil).Once() }},
448 {code: "RNT", expect: func(ctx *MockContext) { ctx.EXPECT().RenameTab().Return(nil).Once() }},
449 {code: "DET", expect: func(ctx *MockContext) { ctx.EXPECT().Detach().Return(nil).Once() }},
450 }
451
452 for _, tt := range tests {
453 t.Run(tt.code, func(t *testing.T) {
454 cmd, ok := ByCode(tt.code)
455 if !ok {
456 t.Fatalf("ByCode(%q) ok = false, want true", tt.code)
457 }
458
459 ctx := NewMockContext(t)
460 tt.expect(ctx)
461 args := []string(nil)
462 if tt.code == "JRS" {
463 args = []string{"1"}
464 }
465 if err := cmd.Run(ctx, args); err != nil {
466 t.Fatalf("Run() error = %v, want nil", err)
467 }
468 })
469 }
470}