v0.29.0 55fcc8f

text_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
16// textCases are the rules shared by every text; multi says whether line breaks
17// are allowed.
18func TestParseMRTitle(t *testing.T) {
19	t.Parallel()
20	tests := []struct {
21		name   string
22		in     string
23		reason string // "" when valid
24	}{
25		{"plain", "Add the login page", ""},
26		{"unicode", "Ajoute la page d’accueil — ça marche 🎉", ""},
27		{"max length in characters", strings.Repeat("é", domain.MaxTitleLen), ""},
28		{"too long", strings.Repeat("a", domain.MaxTitleLen+1), "longer than 120"},
29		{"empty", "", "is empty"},
30		{"blank", "  \u00a0 ", "is empty"},
31		{"line break", "two\nlines", "one line"},
32		{"carriage return", "a\rb", "control character U+000D"},
33		{"tab", "a\tb", "control character U+0009"},
34		{"nul", "a\x00b", "control character U+0000"},
35		{"escape", "\x1b[31mred", "control character U+001B"},
36		{"delete", "a\x7fb", "control character U+007F"},
37		{"c1 control", "a\u0085b", "control character U+0085"},
38		{"invalid utf-8", "a\xffb", "not valid UTF-8"},
39		{"bidi override", "evil \u202e txt.exe", "U+202E"},
40		{"bidi embedding", "a\u202ab", "U+202A"},
41		{"bidi isolate", "a\u2066b", "U+2066"},
42		{"bidi pop isolate", "a\u2069b", "U+2069"},
43		{"line separator", "a\u2028b", "U+2028"},
44		{"paragraph separator", "a\u2029b", "U+2029"},
45		{"left-to-right mark", "a\u200eb", "U+200E"},
46		{"right-to-left mark", "a\u200fb", "U+200F"},
47		{"arabic letter mark", "a\u061cb", "U+061C"},
48		{"zero width joiner is plain text", "a\u200db", ""},
49	}
50	for _, tc := range tests {
51		t.Run(tc.name, func(t *testing.T) {
52			t.Parallel()
53			got, err := domain.ParseMRTitle(tc.in)
54			if tc.reason == "" {
55				require.NoError(t, err)
56				assert.Equal(t, tc.in, got.String())
57				assert.False(t, got.IsZero())
58				return
59			}
60			require.Error(t, err)
61			assert.ErrorIs(t, err, domain.ErrInvalidText)
62			assert.Contains(t, err.Error(), tc.reason)
63			assert.True(t, got.IsZero())
64		})
65	}
66}
67
68func TestParsePlainText(t *testing.T) {
69	t.Parallel()
70	tests := []struct {
71		name   string
72		in     string
73		reason string
74	}{
75		{"empty is valid", "", ""},
76		{"multi-line", "one\ntwo\n\nthree", ""},
77		{"trailing newline", "x\n", ""},
78		{"max length", strings.Repeat("a\n", domain.MaxTextLen/2), ""},
79		{"max length in characters not bytes", strings.Repeat("€", domain.MaxTextLen), ""},
80		{"too long", strings.Repeat("a", domain.MaxTextLen+1), "longer than 8000"},
81		{"carriage return", "a\r\nb", "control character U+000D"},
82		{"tab", "a\tb", "control character U+0009"},
83		{"escape sequence", "a\x1b]0;title\x07", "control character U+001B"},
84		{"bell", "a\x07", "control character U+0007"},
85		{"invalid utf-8", "ok\n\xc3\x28", "not valid UTF-8"},
86		{"bidi override", "x\u202ey", "U+202E"},
87		{"bidi left-to-right isolate", "x\u2066y", "U+2066"},
88		{"bidi first strong isolate", "x\u2068y", "U+2068"},
89		{"line separator", "x\u2028y", "U+2028"},
90		{"paragraph separator", "x\u2029y", "U+2029"},
91		{"left-to-right mark", "x\u200ey", "U+200E"},
92		{"right-to-left mark", "x\u200fy", "U+200F"},
93		{"arabic letter mark", "x\u061cy", "U+061C"},
94		{"zero width joiner is plain text", "x\u200dy", ""},
95	}
96	for _, tc := range tests {
97		t.Run(tc.name, func(t *testing.T) {
98			t.Parallel()
99			got, err := domain.ParsePlainText(tc.in)
100			if tc.reason == "" {
101				require.NoError(t, err)
102				assert.Equal(t, tc.in, got.String())
103				return
104			}
105			require.Error(t, err)
106			assert.ErrorIs(t, err, domain.ErrInvalidText)
107			assert.Contains(t, err.Error(), tc.reason)
108			assert.Empty(t, got.String())
109		})
110	}
111}
112
113func TestPlainTextIsBlank(t *testing.T) {
114	t.Parallel()
115	for in, blank := range map[string]bool{"": true, " \n\n ": true, "x": false, " x ": false} {
116		got, err := domain.ParsePlainText(in)
117		require.NoError(t, err)
118		assert.Equal(t, blank, got.IsBlank(), "%q", in)
119	}
120}
121
122func TestViolations(t *testing.T) {
123	t.Parallel()
124	var v domain.Violations
125	require.NoError(t, v.Err())
126
127	_, err := domain.ParseMRTitle("a\nb")
128	v.AddErr("title", err)
129	v.Add("source_branch", "does not exist")
130	v.AddErr("text", errors.New("boom"))
131
132	err = v.Err()
133	require.ErrorIs(t, err, domain.ErrInvalid)
134	var fe *domain.InvalidFieldsError
135	require.ErrorAs(t, err, &fe)
136	assert.Equal(t, []domain.FieldViolation{
137		{Field: "title", Reason: "must be one line"},
138		{Field: "source_branch", Reason: "does not exist"},
139		{Field: "text", Reason: "boom"},
140	}, fe.Fields)
141	assert.Equal(t, "invalid argument: title must be one line; source_branch does not exist; text boom", err.Error())
142}

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 44

↑↓ moveenter openesc close