v0.32.0 53e25a3

issue_test.go RawHistoryFolder

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain_test
4
5import (
6	"errors"
7	"strings"
8	"testing"
9
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/require"
12
13	"bnema.dev/ermite/internal/domain"
14)
15
16func TestParseIssueTitle(t *testing.T) {
17	t.Parallel()
18	tests := []struct {
19		name, in, want, reason string // reason "" when valid
20	}{
21		{"plain", "Crash on start", "Crash on start", ""},
22		{"max length in code points", strings.Repeat("é", 120), strings.Repeat("é", 120), ""},
23		{"too long", strings.Repeat("a", 121), "", "longer than 120"},
24		{"emoji count as one", strings.Repeat("🎉", 120), strings.Repeat("🎉", 120), ""},
25		{"empty", "", "", "is empty"},
26		{"blank", " \t ", "", "is empty"},
27		{"only controls", "\x01\x02", "", "is empty"},
28		{"newline", "a\nb", "", "one line"},
29		{"carriage return", "a\rb", "", "one line"},
30		{"nfc", "e\u0301", "\u00e9", ""},
31		{"c0 stripped", "a\x00b\x1bc\tz", "abcz", ""},
32		{"c1 stripped", "a\u0085b\u009fc", "abc", ""},
33		{"del stripped", "a\x7fb", "ab", ""},
34		{"bidi stripped", "a\u202ab\u202ec\u2066d\u2069e\u200ef\u200fg\u061ch", "abcdefgh", ""},
35		{"not utf-8", "a\xffb", "", "UTF-8"},
36	}
37	for _, tt := range tests {
38		t.Run(tt.name, func(t *testing.T) {
39			t.Parallel()
40			got, err := domain.ParseIssueTitle(tt.in)
41			if tt.reason == "" {
42				require.NoError(t, err)
43				assert.Equal(t, tt.want, got.String())
44				return
45			}
46			var te *domain.TextError
47			require.True(t, errors.As(err, &te), "%v", err)
48			assert.ErrorIs(t, err, domain.ErrInvalidText)
49			assert.Contains(t, te.Reason, tt.reason)
50		})
51	}
52}
53
54func TestParseIssueBody(t *testing.T) {
55	t.Parallel()
56	tests := []struct {
57		name, in, want, reason string
58	}{
59		{"empty allowed", "", "", ""},
60		{"multiline and tabs kept", "a\n\tb\n", "a\n\tb\n", ""},
61		{"crlf becomes lf", "a\r\nb", "a\nb", ""},
62		{"max length", strings.Repeat("é", 10000), strings.Repeat("é", 10000), ""},
63		{"too long", strings.Repeat("a", 10001), "", "longer than 10000"},
64		{"nfc", "e\u0301\n", "\u00e9\n", ""},
65		{"controls stripped", "a\x00\x08b\u0085c\u009fd\x7f", "abcd", ""},
66		{"bidi stripped", "\u202etxt\u202c \u2067x\u2069", "txt x", ""},
67		{"separators stripped", "a\u2028b\u2029c", "abc", ""},
68		{"not utf-8", "\xc3\x28", "", "UTF-8"},
69	}
70	for _, tt := range tests {
71		t.Run(tt.name, func(t *testing.T) {
72			t.Parallel()
73			got, err := domain.ParseIssueBody(tt.in)
74			if tt.reason == "" {
75				require.NoError(t, err)
76				assert.Equal(t, tt.want, got.String())
77				return
78			}
79			var te *domain.TextError
80			require.True(t, errors.As(err, &te), "%v", err)
81			assert.Contains(t, te.Reason, tt.reason)
82		})
83	}
84}
85
86func TestParseAuthorLabel(t *testing.T) {
87	t.Parallel()
88	tests := []struct {
89		name, in, want, reason string
90	}{
91		{"plain", "Ada", "Ada", ""},
92		{"empty allowed", "", "", ""},
93		{"max length", strings.Repeat("é", 40), strings.Repeat("é", 40), ""},
94		{"too long", strings.Repeat("a", 41), "", "longer than 40"},
95		{"newline", "a\nb", "", "one line"},
96		{"nfc", "e\u0301", "\u00e9", ""},
97		{"stripped", "A\u202ed\x00a", "Ada", ""},
98		{"trimmed", "  Ada ", "Ada", ""},
99		{"not utf-8", "\xff", "", "UTF-8"},
100	}
101	for _, tt := range tests {
102		t.Run(tt.name, func(t *testing.T) {
103			t.Parallel()
104			got, err := domain.ParseAuthorLabel(tt.in)
105			if tt.reason == "" {
106				require.NoError(t, err)
107				assert.Equal(t, tt.want, got)
108				return
109			}
110			var te *domain.TextError
111			require.True(t, errors.As(err, &te), "%v", err)
112			assert.Contains(t, te.Reason, tt.reason)
113		})
114	}
115}
116
117func TestIssuesMode(t *testing.T) {
118	t.Parallel()
119	for _, m := range domain.IssuesModes() {
120		got, err := domain.ParseIssuesMode(m.String())
121		require.NoError(t, err)
122		assert.Equal(t, m, got)
123	}
124	_, err := domain.ParseIssuesMode("all")
125	assert.ErrorIs(t, err, domain.ErrInvalid)
126	assert.Equal(t, domain.IssuesOff, domain.DefaultRepoSettings().Issues)
127	assert.False(t, domain.DefaultRepoSettings().IsZero())
128	assert.True(t, domain.RepoSettings{}.IsZero())
129	assert.Equal(t, domain.IssuesOff, domain.IssuesMode("").OrDefault())
130}
131
132func TestIssueEventKinds(t *testing.T) {
133	t.Parallel()
134	assert.Equal(t, "issue.opened", domain.IssueOpened{}.EventKind())
135	assert.Equal(t, "issue.closed", domain.IssueClosed{}.EventKind())
136	assert.Equal(t, "issue.reopened", domain.IssueReopened{}.EventKind())
137	assert.Equal(t, "issue.commented", domain.IssueCommented{}.EventKind())
138}

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 3

Tags 43

↑↓ moveenter openesc close