v0.9.0 e5e4ded

terminal_focus_test.go RawHistoryFolder

1package client
2
3import (
4	"context"
5	"errors"
6	"io"
7	"testing"
8	"time"
9
10	"github.com/stretchr/testify/require"
11
12	"github.com/bnema/vev/internal/domain"
13	"github.com/bnema/vev/internal/protocol"
14)
15
16func TestStripTerminalFocusReports(t *testing.T) {
17	tests := []struct {
18		name      string
19		start     domain.TerminalFocus
20		input     string
21		want      string
22		wantFocus domain.TerminalFocus
23		changed   bool
24	}{
25		{name: "plain input passes through", input: "ls\r", want: "ls\r"},
26		{name: "focus in only", input: "\x1b[I", want: "", wantFocus: domain.TerminalFocusFocused, changed: true},
27		{name: "focus out only", start: domain.TerminalFocusFocused, input: "\x1b[O", want: "", wantFocus: domain.TerminalFocusUnfocused, changed: true},
28		{name: "last report wins", input: "\x1b[I\x1b[O", want: "", wantFocus: domain.TerminalFocusUnfocused, changed: true},
29		{name: "keys around a report keep their order", input: "a\x1b[Ib", want: "ab", wantFocus: domain.TerminalFocusFocused, changed: true},
30		{name: "arrow keys and csi are untouched", input: "\x1b[A\x1b[1;5C\x1b[<0;1;1M", want: "\x1b[A\x1b[1;5C\x1b[<0;1;1M"},
31		{name: "a lone escape is never withheld", input: "\x1b", want: "\x1b"},
32		{name: "a split report is forwarded as typed", input: "\x1b[", want: "\x1b["},
33		{name: "a repeated state changes nothing", start: domain.TerminalFocusFocused, input: "\x1b[I", want: "", wantFocus: domain.TerminalFocusFocused},
34	}
35	for _, tt := range tests {
36		t.Run(tt.name, func(t *testing.T) {
37			state := newTerminalFocusState()
38			state.set(tt.start)
39			_, changed := state.Watch()
40
41			got := stripTerminalFocusReports(state, []byte(tt.input))
42
43			require.Equal(t, tt.want, string(got))
44			want := tt.wantFocus
45			if want == domain.TerminalFocusUnknown {
46				want = tt.start
47			}
48			require.Equal(t, want, state.Focus())
49			select {
50			case <-changed:
51				require.True(t, tt.changed, "no change was expected")
52			default:
53				require.False(t, tt.changed, "a change was expected")
54			}
55		})
56	}
57}
58
59func TestFocusReporterSendsKnownChangesOnce(t *testing.T) {
60	state := newTerminalFocusState()
61	reporter := &focusReporter{state: state}
62
63	_, ok := reporter.next()
64	require.False(t, ok, "unknown focus is never sent")
65
66	steps := []struct {
67		focus  domain.TerminalFocus
68		want   protocol.TerminalFocus
69		wantOK bool
70	}{
71		{focus: domain.TerminalFocusFocused, want: protocol.TerminalFocus{Focus: domain.TerminalFocusFocused}, wantOK: true},
72		{focus: domain.TerminalFocusUnfocused, want: protocol.TerminalFocus{Focus: domain.TerminalFocusUnfocused}, wantOK: true},
73		{focus: domain.TerminalFocusUnfocused},
74	}
75	for _, step := range steps {
76		wake := reporter.changed
77		state.set(step.focus)
78		if step.wantOK {
79			select {
80			case <-wake:
81			default:
82				t.Fatalf("a change to %s must wake the reporter", step.focus)
83			}
84		}
85		got, ok := reporter.next()
86		require.Equal(t, step.wantOK, ok)
87		require.Equal(t, step.want, got)
88	}
89}
90
91// TestTerminalInputPumpStripsFocusReports proves focus reports never reach an
92// attachment: a read that only carried a report delivers nothing, the focus
93// the terminal reported is readable on the foreground, and later keys arrive
94// intact.
95func TestTerminalInputPumpStripsFocusReports(t *testing.T) {
96	pr, pw := io.Pipe()
97	pump := newTerminalInputPump(pr)
98	pump.start()
99	defer pump.stop()
100	defer pw.Close()
101	host := newWorkerTestHost(newWorkerTestTerminal(), pump, nil)
102
103	observed := make(chan []byte, 1)
104	focus := make(chan domain.TerminalFocus, 1)
105	worker := &fakeWorker{run: func(ctx context.Context, fg AttachmentForeground) AttachmentEvent {
106		event, ok := fg.Input(ctx)
107		if !ok {
108			return AttachmentEvent{Kind: AttachmentEventFailed, Err: errors.New("no input")}
109		}
110		fg.AckInput()
111		observed <- append([]byte(nil), event.Data...)
112		focus <- attachmentTerminalFocus(fg).Focus()
113		return AttachmentEvent{Kind: AttachmentEventEnded}
114	}}
115
116	go func() {
117		_, _ = pw.Write([]byte("\x1b[O"))
118		_, _ = pw.Write([]byte("\x1b[Iab"))
119	}()
120	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
121	defer cancel()
122	_, adopted := host.Run(ctx, AttachmentToken{Generation: 1, Attempt: 1}, worker, newWorkerTestStream())
123	require.True(t, adopted)
124
125	require.Equal(t, []byte("ab"), <-observed, "the focus-only read is never delivered")
126	require.Equal(t, domain.TerminalFocusFocused, <-focus)
127}
128
129func TestNilTerminalFocusStateIsUnknown(t *testing.T) {
130	var state *TerminalFocusState
131	focus, changed := state.Watch()
132	require.Equal(t, domain.TerminalFocusUnknown, focus)
133	require.Nil(t, changed)
134	require.Equal(t, domain.TerminalFocusUnknown, state.Focus())
135}
136
137// TestAttachmentDeclaresFocusInHelloThenReportsChanges proves the daemon
138// learns a terminal's focus before its first paint: Hello carries the focus
139// already known, and each later change follows as TerminalFocus.
140func TestAttachmentDeclaresFocusInHelloThenReportsChanges(t *testing.T) {
141	pump := newTerminalInputPump(blockingReader{})
142	pump.focus.set(domain.TerminalFocusUnfocused)
143	stream := newSessionTestStream()
144	host := newWorkerTestHost(newWorkerTestTerminal(), pump, nil)
145	worker, err := newSessionAttachmentWorker(sessionTestWorkerConfig(sessionTestRequest(true)))
146	require.NoError(t, err)
147	ctx, cancel := context.WithCancel(context.Background())
148	defer cancel()
149	go func() { _, _ = host.Run(ctx, AttachmentToken{Generation: 4, Attempt: 1}, worker, stream) }()
150
151	hello := awaitHello(t, stream)
152	require.Equal(t, domain.TerminalFocusUnfocused, hello.TerminalFocus)
153	require.NoError(t, protocol.ValidateHello(hello))
154
155	stream.deliver(protocol.Welcome{SessionName: "alpha"})
156	stream.deliver(sessionTestOutput(1, "\x1b[Hready"))
157	pump.focus.set(domain.TerminalFocusFocused)
158	require.Eventually(t, func() bool {
159		for _, message := range stream.messages() {
160			if message == (protocol.TerminalFocus{Focus: domain.TerminalFocusFocused}) {
161				return true
162			}
163		}
164		return false
165	}, 5*time.Second, time.Millisecond, "a focus change after attach is reported")
166}
167
168// blockingReader is a terminal that never sends input.
169type blockingReader struct{}
170
171func (blockingReader) Read([]byte) (int, error) { select {} }

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 12

Tags 11

↑↓ moveenter openesc close