v0.3.0 9fdf386

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

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 43

↑↓ moveenter openesc close