v0.7.0 36c429d

event_test.go RawHistoryFolder

1package browser
2
3import (
4	"encoding/json"
5	"strings"
6	"testing"
7
8	"github.com/stretchr/testify/require"
9)
10
11func TestDecodeEventValidatesTheClosedSchema(t *testing.T) {
12	event, err := DecodeEvent([]byte(`{"schemaVersion":2,"type":"text","text":"é"}`), EventLimits{})
13	require.NoError(t, err)
14	require.Equal(t, EventText, event.Kind)
15	require.Equal(t, "é", event.Text.Text)
16
17	_, err = DecodeEvent([]byte(`{"schemaVersion":2,"type":"text","text":"x","unknown":true}`), EventLimits{})
18	require.Error(t, err)
19
20	_, err = DecodeEvent([]byte(`{"schemaVersion":2,"type":"paste","text":"toolong"}`), EventLimits{MaxPasteBytes: 3})
21	require.ErrorIs(t, err, ErrEventLimit)
22
23	_, err = DecodeEvent([]byte{'{', '"', 'x', '"', ':', '"', 0xff, '"', '}'}, EventLimits{})
24	require.ErrorContains(t, err, "not valid UTF-8")
25
26	_, err = DecodeEvent([]byte(`{"schemaVersion":2,"type":"resize","columns":80,"rows":24,"pixelWidth":800,"pixelHeight":480,"cellWidth":10,"cellHeight":20,"devicePixelRatio":1}`), EventLimits{})
27	require.NoError(t, err)
28
29	for _, data := range []string{
30		`{"schemaVersion":2,"type":"focus"}`,
31		`{"schemaVersion":2,"type":"wheel"}`,
32	} {
33		_, err = DecodeEvent([]byte(data), EventLimits{})
34		require.Error(t, err)
35	}
36}
37
38func TestDecodeEventAcceptsRuntimeEventKinds(t *testing.T) {
39	tests := []struct {
40		kind EventKind
41		json string
42	}{
43		{EventKey, `{"schemaVersion":2,"type":"key","key":"ArrowUp","code":"ArrowUp","alt":false,"ctrl":false,"meta":false,"shift":false,"repeat":false,"location":0}`},
44		{EventPaste, `{"schemaVersion":2,"type":"paste","text":"hello"}`},
45		{EventPointer, `{"schemaVersion":2,"type":"pointer","action":"down","button":0,"buttons":1,"row":0,"column":0,"x":1,"y":2,"alt":false,"ctrl":false,"meta":false,"shift":false}`},
46		{EventWheel, `{"schemaVersion":2,"type":"wheel","deltaX":1,"deltaY":2,"deltaMode":0,"row":0,"column":0,"alt":false,"ctrl":false,"meta":false,"shift":false}`},
47		{EventFocus, `{"schemaVersion":2,"type":"focus","focused":true}`},
48	}
49	for _, test := range tests {
50		t.Run(string(test.kind), func(t *testing.T) {
51			event, err := DecodeEvent([]byte(test.json), EventLimits{})
52			require.NoError(t, err)
53			require.Equal(t, test.kind, event.Kind)
54			if test.kind == EventKey {
55				require.Equal(t, "ArrowUp", event.Key.Key)
56			}
57		})
58	}
59}
60
61func TestDefaultEventLimitsAcceptEscapedMaximumPaste(t *testing.T) {
62	limits := DefaultEventLimits()
63	text := strings.Repeat("\x00", limits.MaxPasteBytes)
64	data, err := json.Marshal(textWire{
65		SchemaVersion: EventSchemaVersion,
66		Kind:          EventPaste,
67		Text:          &text,
68	})
69	require.NoError(t, err)
70
71	event, err := DecodeEvent(data, EventLimits{})
72	require.NoError(t, err)
73	require.NotNil(t, event.Paste)
74	require.Len(t, event.Paste.Text, limits.MaxPasteBytes)
75}
76
77func TestNormalizeEventLimitsRejectsImpossibleEnvelope(t *testing.T) {
78	_, err := normalizeEventLimits(EventLimits{MaxEventBytes: 69, MaxTextBytes: 1, MaxPasteBytes: 1})
79	require.ErrorIs(t, err, ErrInvalidEventLimits)
80
81	_, err = normalizeEventLimits(EventLimits{MaxTextBytes: -1})
82	require.ErrorIs(t, err, ErrInvalidEventLimits)
83}
84
85func TestDecodeEventRejectsMissingRequiredFields(t *testing.T) {
86	tests := []string{
87		`{"schemaVersion":2,"type":"text"}`,
88		`{"schemaVersion":2,"type":"key","code":"ArrowUp"}`,
89		`{"schemaVersion":2,"type":"pointer","action":"down"}`,
90		`{"schemaVersion":2,"type":"wheel"}`,
91		`{"schemaVersion":2,"type":"resize","columns":80,"rows":24}`,
92		`{"schemaVersion":2,"type":"focus"}`,
93	}
94	for _, data := range tests {
95		_, err := DecodeEvent([]byte(data), EventLimits{})
96		require.Error(t, err, data)
97	}
98}
99
100func TestDecodeEventRejectsInvalidRuntimePayloads(t *testing.T) {
101	tests := map[string]string{
102		"schema":                `{"schemaVersion":3,"type":"focus","focused":true}`,
103		"pointer button":        `{"schemaVersion":2,"type":"pointer","action":"down","button":5,"buttons":1,"row":0,"column":0,"x":1,"y":2}`,
104		"pointer buttons":       `{"schemaVersion":2,"type":"pointer","action":"down","button":0,"buttons":32,"row":0,"column":0,"x":1,"y":2}`,
105		"wheel mode":            `{"schemaVersion":2,"type":"wheel","deltaX":1,"deltaY":2,"deltaMode":3,"row":0,"column":0}`,
106		"resize dimension":      `{"schemaVersion":2,"type":"resize","columns":0,"rows":24,"pixelWidth":800,"pixelHeight":480,"cellWidth":10,"cellHeight":20,"devicePixelRatio":1}`,
107		"non-finite coordinate": `{"schemaVersion":2,"type":"pointer","action":"move","button":-1,"buttons":0,"row":0,"column":0,"x":1e999,"y":2}`,
108		"trailing value":        `{"schemaVersion":2,"type":"focus","focused":true} {}`,
109	}
110	for name, data := range tests {
111		t.Run(name, func(t *testing.T) {
112			_, err := DecodeEvent([]byte(data), EventLimits{})
113			require.Error(t, err)
114		})
115	}
116}
117
118func FuzzDecodeEvent(f *testing.F) {
119	f.Add([]byte(`{"schemaVersion":2,"type":"focus","focused":true}`))
120	f.Add([]byte(`{"schemaVersion":2,"type":"text","text":"hello"}`))
121	f.Add([]byte(`not json`))
122	f.Add([]byte(`{"schemaVersion":2,"type":"text","text":"` + strings.Repeat("x", 4<<10) + `"}`))
123	f.Fuzz(func(t *testing.T, data []byte) {
124		event, err := DecodeEvent(data, EventLimits{MaxEventBytes: 4 << 10, MaxTextBytes: 512, MaxPasteBytes: 512})
125		if len(data) > 4<<10 {
126			require.ErrorIs(t, err, ErrEventLimit)
127			return
128		}
129		if err != nil {
130			return
131		}
132		payloads := map[EventKind]bool{
133			EventText:    event.Text != nil,
134			EventKey:     event.Key != nil,
135			EventPaste:   event.Paste != nil,
136			EventPointer: event.Pointer != nil,
137			EventWheel:   event.Wheel != nil,
138			EventResize:  event.Resize != nil,
139			EventFocus:   event.Focus != nil,
140		}
141		set := 0
142		for _, present := range payloads {
143			if present {
144				set++
145			}
146		}
147		if set != 1 || !payloads[event.Kind] {
148			t.Fatalf("decoded event violates the closed union: %#v", event)
149		}
150	})
151}

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 13

↑↓ moveenter openesc close