v0.2.1 7e79979

cli_test.go RawHistoryFolder

1package cli_test
2
3import (
4	"bytes"
5	"errors"
6	"flag"
7	"testing"
8	"time"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	"github.com/bnema/nefercap/internal/adapters/cli"
14	"github.com/bnema/nefercap/internal/ports"
15)
16
17func TestParseValid(t *testing.T) {
18	cases := []struct {
19		name string
20		args []string
21		want cli.Options
22	}{
23		{"all-in-one", nil, cli.Options{Command: cli.AllInOne}},
24		{"all-in-one debug", []string{"-debug"}, cli.Options{Command: cli.AllInOne, Debug: true}},
25		{"shot clipboard", []string{"shot", "-clipboard"}, cli.Options{Command: cli.Shot, Clipboard: true}},
26		{"shot clipboard file", []string{"shot", "-clipboard", "-file", "a.png"},
27			cli.Options{Command: cli.Shot, Clipboard: true, Path: "a.png"}},
28		{"shot", []string{"shot"}, cli.Options{Command: cli.Shot}},
29		{"shot all", []string{"shot", "-file", "a.png", "-output", "DP-1", "-debug"},
30			cli.Options{Command: cli.Shot, Path: "a.png", Output: "DP-1", Debug: true}},
31		{"rec defaults", []string{"rec"}, cli.Options{Command: cli.Rec, Video: ports.VideoSettings{FPS: 30}}},
32		{"rec all", []string{"rec", "-file", "a.mkv", "-output", "eDP-1", "-fps", "60", "-size", "1280x720", "-duration", "10s", "-debug"},
33			cli.Options{Command: cli.Rec, Path: "a.mkv", Output: "eDP-1", Duration: 10 * time.Second, Debug: true,
34				Video: ports.VideoSettings{FPS: 60, Width: 1280, Height: 720}}},
35		{"stop", []string{"stop"}, cli.Options{Command: cli.Stop}},
36		{"stop debug", []string{"stop", "-debug"}, cli.Options{Command: cli.Stop, Debug: true}},
37		{"status", []string{"status"}, cli.Options{Command: cli.Status}},
38		{"status debug", []string{"status", "-debug"}, cli.Options{Command: cli.Status, Debug: true}},
39		{"outputs", []string{"outputs"}, cli.Options{Command: cli.Outputs}},
40		{"outputs debug", []string{"outputs", "-debug"}, cli.Options{Command: cli.Outputs, Debug: true}},
41		{"screenshot", []string{"screenshot", "-file", "a.png"},
42			cli.Options{Command: cli.Screenshot, Path: "a.png"}},
43		{"screenshot all", []string{"screenshot", "-output", "DP-1", "-region", "10,20,300x400", "-file", "a.png", "-debug"},
44			cli.Options{Command: cli.Screenshot, Output: "DP-1", Path: "a.png", Debug: true,
45				Region: ports.Region{X: 10, Y: 20, Width: 300, Height: 400}}},
46		{"record defaults", []string{"record", "-file", "a.mkv"},
47			cli.Options{Command: cli.Record, Path: "a.mkv", Video: ports.VideoSettings{FPS: 30}}},
48		{"record all", []string{"record", "-file", "a.mkv", "-fps", "60", "-size", "1280x720", "-duration", "1m30s", "-output", "eDP-1", "-region", "0,0,640x480"},
49			cli.Options{Command: cli.Record, Path: "a.mkv", Output: "eDP-1", Duration: 90 * time.Second,
50				Region: ports.Region{Width: 640, Height: 480},
51				Video:  ports.VideoSettings{FPS: 60, Width: 1280, Height: 720}}},
52		{"record zero duration", []string{"record", "-file", "a.mkv", "-duration", "0"},
53			cli.Options{Command: cli.Record, Path: "a.mkv", Video: ports.VideoSettings{FPS: 30}}},
54		{"record fps bounds", []string{"record", "-file", "a.mkv", "-fps", "120"},
55			cli.Options{Command: cli.Record, Path: "a.mkv", Video: ports.VideoSettings{FPS: 120}}},
56		{"double dash flag", []string{"record", "--file", "a.mkv", "--fps=1"},
57			cli.Options{Command: cli.Record, Path: "a.mkv", Video: ports.VideoSettings{FPS: 1}}},
58	}
59	for _, tc := range cases {
60		t.Run(tc.name, func(t *testing.T) {
61			var out bytes.Buffer
62			got, err := cli.Parse(tc.args, &out)
63			require.NoError(t, err)
64			assert.Equal(t, tc.want, got)
65			assert.Empty(t, out.String())
66		})
67	}
68}
69
70func TestParseUsageErrors(t *testing.T) {
71	cases := map[string][]string{
72		"unknown command":          {"snap"},
73		"positional after command": {"outputs", "extra"},
74		"positional default":       {"extra"},
75		"positional shot":          {"shot", "extra"},
76		"positional stop":          {"stop", "extra"},
77		"gui removed":              {"gui"},
78		"gui removed with flag":    {"gui", "-debug"},
79		"empty shot file":          {"shot", "-file", ""},
80		"empty rec file":           {"rec", "-file", ""},
81		"empty default file":       {"-file", ""},
82		"region on shot":           {"shot", "-region", "0,0,10x10"},
83		"region on rec":            {"rec", "-region", "0,0,10x10"},
84		"region on default":        {"-region", "0,0,10x10"},
85		"fps on shot":              {"shot", "-fps", "30"},
86		"size on shot":             {"shot", "-size", "2x2"},
87		"duration on shot":         {"shot", "-duration", "1s"},
88		"fps zero on rec":          {"rec", "-fps", "0"},
89		"fps high on rec":          {"rec", "-fps", "121"},
90		"size odd on rec":          {"rec", "-size", "641x480"},
91		"size huge on rec":         {"rec", "-size", "16384x16384"},
92		"duration negative on rec": {"rec", "-duration", "-1s"},
93		"file on stop":             {"stop", "-file", "a"},
94		"output on stop":           {"stop", "-output", "DP-1"},
95		"file on status":           {"status", "-file", "a"},
96		"output on status":         {"status", "-output", "DP-1"},
97		"fps on status":            {"status", "-fps", "30"},
98		"file on outputs":          {"outputs", "-file", "a"},
99		"positional after flags":   {"record", "-file", "a.mkv", "extra"},
100		"unknown flag":             {"-nope"},
101		"unknown flag in command":  {"outputs", "-nope"},
102		"screenshot missing file":  {"screenshot"},
103		"record missing file":      {"record", "-fps", "30"},
104		"empty file":               {"screenshot", "-file", ""},
105		"capture flag on outputs":  {"outputs", "-output", "DP-1"},
106		"fps on screenshot":        {"screenshot", "-file", "a.png", "-fps", "30"},
107		"duration on screenshot":   {"screenshot", "-file", "a.png", "-duration", "1s"},
108		"size on screenshot":       {"screenshot", "-file", "a.png", "-size", "2x2"},
109		"fps zero":                 {"record", "-file", "a", "-fps", "0"},
110		"fps high":                 {"record", "-file", "a", "-fps", "121"},
111		"fps text":                 {"record", "-file", "a", "-fps", "fast"},
112		"size odd width":           {"record", "-file", "a", "-size", "641x480"},
113		"size odd height":          {"record", "-file", "a", "-size", "640x481"},
114		"size zero":                {"record", "-file", "a", "-size", "0x0"},
115		"size too large":           {"record", "-file", "a", "-size", "16386x2"},
116		"size frame too large":     {"record", "-file", "a", "-size", "16384x16384"},
117		"size missing height":      {"record", "-file", "a", "-size", "640"},
118		"size negative":            {"record", "-file", "a", "-size", "-2x-2"},
119		"size junk":                {"record", "-file", "a", "-size", "axb"},
120		"size signed":              {"record", "-file", "a", "-size", "+2x2"},
121		"duration negative":        {"record", "-file", "a", "-duration", "-1s"},
122		"duration bare number":     {"record", "-file", "a", "-duration", "5"},
123		"region empty parts":       {"screenshot", "-file", "a", "-region", ",,x"},
124		"region missing size":      {"screenshot", "-file", "a", "-region", "1,2"},
125		"region missing y":         {"screenshot", "-file", "a", "-region", "1,10x10"},
126		"region extra part":        {"screenshot", "-file", "a", "-region", "1,2,3,10x10"},
127		"region negative x":        {"screenshot", "-file", "a", "-region", "-1,0,10x10"},
128		"region negative y":        {"screenshot", "-file", "a", "-region", "0,-1,10x10"},
129		"region zero width":        {"screenshot", "-file", "a", "-region", "0,0,0x10"},
130		"region zero height":       {"screenshot", "-file", "a", "-region", "0,0,10x0"},
131		"region overflow":          {"screenshot", "-file", "a", "-region", "0,0,99999999999x1"},
132		"region sum overflow":      {"screenshot", "-file", "a", "-region", "2147483647,0,1x1"},
133		"region signed":            {"screenshot", "-file", "a", "-region", "+1,0,10x10"},
134		"region uppercase X":       {"screenshot", "-file", "a", "-region", "0,0,10X10"},
135		"region spaces":            {"screenshot", "-file", "a", "-region", "0, 0, 10x10"},
136	}
137	for name, args := range cases {
138		t.Run(name, func(t *testing.T) {
139			var out bytes.Buffer
140			got, err := cli.Parse(args, &out)
141			require.Error(t, err)
142			assert.ErrorIs(t, err, cli.ErrUsage)
143			assert.NotErrorIs(t, err, flag.ErrHelp)
144			assert.Equal(t, cli.Options{}, got)
145			assert.Empty(t, out.String(), "errors are reported by the caller")
146		})
147	}
148}
149
150func TestAllInOneRejectsFlags(t *testing.T) {
151	for _, args := range [][]string{
152		{"-file", "a.png"}, {"-output", "DP-1"}, {"-clipboard"}, {"-region", "0,0,1x1"}, {"-fps", "30"},
153		{"-size", "2x2"}, {"-duration", "1s"}, {"-nope"}, {"-debug", "-file", "a.png"}, {"-file", ""},
154	} {
155		got, err := cli.Parse(args, nil)
156		require.ErrorIs(t, err, cli.ErrUsage, args)
157		assert.ErrorContains(t, err, "all-in-one mode takes no flags except -debug")
158		assert.ErrorContains(t, err, "use shot, rec, screenshot or record for per-run overrides")
159		assert.Equal(t, cli.Options{}, got)
160	}
161}
162
163func TestParseErrorNamesProblem(t *testing.T) {
164	_, err := cli.Parse([]string{"record"}, nil)
165	assert.ErrorContains(t, err, "-file is required")
166	_, err = cli.Parse([]string{"snap"}, nil)
167	assert.ErrorContains(t, err, `unknown command "snap"`)
168	_, err = cli.Parse([]string{"screenshot", "-file", "a", "-region", "1,2"}, nil)
169	assert.ErrorContains(t, err, "X,Y,WxH")
170}
171
172func TestParseHelp(t *testing.T) {
173	for _, args := range [][]string{{"-h"}, {"--help"}, {"-help"}, {"record", "--help"}, {"outputs", "-h"}, {"shot", "-h"}, {"rec", "-h"}, {"stop", "-h"}, {"status", "--help"}} {
174		var out bytes.Buffer
175		got, err := cli.Parse(args, &out)
176		assert.True(t, errors.Is(err, flag.ErrHelp), args)
177		assert.Equal(t, cli.Options{}, got)
178		assert.Equal(t, cli.Usage(), out.String())
179	}
180}
181
182func TestUsageMentionsEverything(t *testing.T) {
183	u := cli.Usage()
184	for _, s := range []string{"shot", "rec", "stop", "status", "outputs", "screenshot", "record",
185		"layer-shell", "drag", "click", "Escape", "Enter", "workspace", "1..9", "  r ", "  m ", "  w ", "  g ", "-output", "-region", "-file", "-fps", "-size", "-duration", "-debug"} {
186		assert.Contains(t, u, s)
187	}
188	assert.NotContains(t, u, "  gui ")
189}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 9

↑↓ moveenter openesc close