v0.8.1 1f54f61

terminal_test.go RawHistoryFolder

1package uiterm
2
3import (
4	"context"
5	"errors"
6	"io"
7	"testing"
8	"time"
9
10	"github.com/bnema/vev/internal/domain"
11	"github.com/bnema/vev/internal/ports"
12)
13
14func TestTerminalPublishesOwnedSnapshotAtFlush(t *testing.T) {
15	terminal := newTestTerminal(t, 4, 2)
16	terminal.BeginOutput(ports.UIContext{AttachmentHandle: "attachment", Generation: 2, Status: ports.UIStatusAttached})
17	if _, err := terminal.Write([]byte("\x1b[1;31;48;2;1;2;3mA界")); err != nil {
18		t.Fatal(err)
19	}
20	if err := terminal.Flush(); err != nil {
21		t.Fatal(err)
22	}
23	if _, err := terminal.Snapshot(); err == nil {
24		t.Fatal("nested flush exposed an output transaction")
25	}
26	terminal.EndOutput(true)
27
28	snapshot, err := terminal.Snapshot()
29	if err != nil {
30		t.Fatal(err)
31	}
32	if snapshot.Revision != 1 || snapshot.Columns != 4 || snapshot.Rows != 2 {
33		t.Fatalf("snapshot metadata = %#v", snapshot)
34	}
35	if snapshot.Context.Generation != 2 || snapshot.Cells[0].Text != "A" || !snapshot.Cells[0].Style.Bold {
36		t.Fatalf("first cell/context = %#v / %#v", snapshot.Cells[0], snapshot.Context)
37	}
38	if snapshot.Cells[0].Style.Foreground.Kind != ports.UIColorIndexed || snapshot.Cells[0].Style.Foreground.Index != 1 {
39		t.Fatalf("foreground = %#v", snapshot.Cells[0].Style.Foreground)
40	}
41	if snapshot.Cells[0].Style.Background != (ports.UIColor{Kind: ports.UIColorRGB, R: 1, G: 2, B: 3}) {
42		t.Fatalf("background = %#v", snapshot.Cells[0].Style.Background)
43	}
44	if snapshot.Cells[1].Text != "界" || snapshot.Cells[1].Width != 2 || !snapshot.Cells[2].Continuation {
45		t.Fatalf("wide cells = %#v %#v", snapshot.Cells[1], snapshot.Cells[2])
46	}
47
48	snapshot.Cells[0].Text = "mutated"
49	again, err := terminal.Snapshot()
50	if err != nil {
51		t.Fatal(err)
52	}
53	if again.Cells[0].Text != "A" {
54		t.Fatal("caller mutated published snapshot")
55	}
56}
57
58func TestTerminalEnterRawUsesVisualInitialization(t *testing.T) {
59	terminal := newTestTerminal(t, 4, 2)
60	restore, err := terminal.EnterRaw()
61	if err != nil {
62		t.Fatal(err)
63	}
64	snapshot, err := terminal.Snapshot()
65	if err != nil {
66		t.Fatal(err)
67	}
68	if snapshot.AutoWrap || snapshot.Cursor.Visible {
69		t.Fatalf("enter modes = autoWrap %v cursorVisible %v", snapshot.AutoWrap, snapshot.Cursor.Visible)
70	}
71	if err := restore(); err != nil {
72		t.Fatal(err)
73	}
74	if err := restore(); err != nil {
75		t.Fatalf("second restore = %v", err)
76	}
77}
78
79func TestTerminalQueuesTerminalResponsesOutsideScreenWrite(t *testing.T) {
80	terminal := newTestTerminal(t, 4, 2)
81	if _, err := terminal.Write([]byte("\x1b[?7$p")); err != nil {
82		t.Fatal(err)
83	}
84
85	result := make(chan string, 1)
86	go func() {
87		buffer := make([]byte, len("\x1b[?7;1$y"))
88		_, err := io.ReadFull(terminal.In(), buffer)
89		if err != nil {
90			result <- err.Error()
91			return
92		}
93		result <- string(buffer)
94	}()
95	select {
96	case got := <-result:
97		if got != "\x1b[?7;1$y" {
98			t.Fatalf("response = %q", got)
99		}
100	case <-time.After(time.Second):
101		t.Fatal("terminal response was not drained")
102	}
103}
104
105func TestTerminalPublishesGeometryChangesImmediatelyOutsideOutputTransaction(t *testing.T) {
106	terminal := newTestTerminal(t, 4, 2)
107	terminal.ObserveTerminalResize(domain.Geometry{Size: domain.Size{Cols: 6, Rows: 3}})
108	snapshot, err := terminal.Snapshot()
109	if err != nil {
110		t.Fatal(err)
111	}
112	if snapshot.Columns != 6 || snapshot.Rows != 3 || snapshot.Revision == 0 {
113		t.Fatalf("resize snapshot = %#v", snapshot)
114	}
115}
116
117func TestTerminalRecoversAfterOversizedGeometry(t *testing.T) {
118	terminal := newTestTerminal(t, 4, 2)
119	terminal.ObserveTerminalResize(domain.Geometry{Size: domain.Size{Cols: MaxColumns + 1, Rows: 2}})
120	_, err := terminal.Snapshot()
121	var uiErr *ports.UIError
122	if !errors.As(err, &uiErr) || uiErr.Code != ports.UIErrCaptureTooLarge {
123		t.Fatalf("oversized Snapshot() error = %v, want capture_too_large", err)
124	}
125
126	terminal.ObserveTerminalResize(domain.Geometry{Size: domain.Size{Cols: 6, Rows: 3}})
127	snapshot, err := terminal.Snapshot()
128	if err != nil {
129		t.Fatal(err)
130	}
131	if snapshot.Columns != 6 || snapshot.Rows != 3 || snapshot.Revision == 0 {
132		t.Fatalf("recovered resize snapshot = %#v", snapshot)
133	}
134}
135
136func TestTerminalSignalsCaptureUnavailable(t *testing.T) {
137	terminal := newTestTerminal(t, 4, 2)
138	terminal.BeginOutput(ports.UIContext{})
139	_, _ = terminal.Write([]byte("data"))
140	terminal.EndOutput(false)
141	select {
142	case <-terminal.Changes():
143	case <-time.After(time.Second):
144		t.Fatal("capture unavailability did not signal Changes")
145	}
146}
147
148func TestTerminalFailedTransactionMakesCaptureUnavailable(t *testing.T) {
149	terminal := newTestTerminal(t, 4, 2)
150	terminal.BeginOutput(ports.UIContext{})
151	_, _ = terminal.Write([]byte("data"))
152	terminal.EndOutput(false)
153	if _, err := terminal.Snapshot(); !errors.Is(err, ports.ErrUIUnavailable) {
154		t.Fatalf("Snapshot error = %v", err)
155	}
156}
157
158func TestMirrorReportsOversizedGeometryWithoutRejectingTheTerminal(t *testing.T) {
159	ctx := context.Background()
160	mirror, err := NewMirror(ctx, domain.Geometry{Size: domain.Size{Cols: MaxColumns + 1, Rows: 2}}, "attachment")
161	if err != nil {
162		t.Fatal(err)
163	}
164	defer mirror.Close()
165
166	if _, err := mirror.Write([]byte("still interactive")); err != nil {
167		t.Fatalf("Write() error = %v", err)
168	}
169	if err := mirror.Flush(); err != nil {
170		t.Fatalf("Flush() error = %v", err)
171	}
172	_, err = mirror.Snapshot()
173	var uiErr *ports.UIError
174	if !errors.As(err, &uiErr) || uiErr.Code != ports.UIErrCaptureTooLarge {
175		t.Fatalf("Snapshot() error = %v, want capture_too_large", err)
176	}
177
178	validMirror, err := NewMirror(ctx, domain.Geometry{Size: domain.Size{Cols: 4, Rows: 2}}, "attachment")
179	if err != nil {
180		t.Fatal(err)
181	}
182	defer validMirror.Close()
183	validMirror.ObserveTerminalResize(domain.Geometry{Size: domain.Size{Cols: MaxColumns + 1, Rows: 2}})
184	if _, err := validMirror.Write([]byte("output while oversized")); err != nil {
185		t.Fatalf("oversized mirror Write() error = %v", err)
186	}
187	if err := validMirror.Flush(); err != nil {
188		t.Fatalf("oversized mirror Flush() error = %v", err)
189	}
190	validMirror.ObserveTerminalResize(domain.Geometry{Size: domain.Size{Cols: 4, Rows: 2}})
191	_, err = validMirror.Snapshot()
192	if !errors.As(err, &uiErr) || uiErr.Code != ports.UIErrCaptureTooLarge {
193		t.Fatalf("recovered mirror Snapshot() error = %v, want capture_too_large", err)
194	}
195}
196
197func TestTerminalRejectsInvalidGeometry(t *testing.T) {
198	tests := []domain.Geometry{
199		{Size: domain.Size{Cols: 0, Rows: 1}},
200		{Size: domain.Size{Cols: 1, Rows: 0}},
201		{Size: domain.Size{Cols: MaxColumns + 1, Rows: 1}},
202		{Size: domain.Size{Cols: 1, Rows: MaxRows + 1}},
203	}
204	for _, geometry := range tests {
205		if terminal, err := New(context.Background(), geometry, "handle"); err == nil {
206			terminal.Close()
207			t.Fatalf("New(%#v) succeeded", geometry)
208		}
209	}
210}
211
212func newTestTerminal(t *testing.T, columns, rows int) *Terminal {
213	t.Helper()
214	terminal, err := New(context.Background(), domain.Geometry{Size: domain.Size{Cols: columns, Rows: rows}}, "attachment")
215	if err != nil {
216		t.Fatal(err)
217	}
218	t.Cleanup(terminal.Close)
219	return terminal
220}
221
222// TestTerminalUnattachedPublicationKeepsStatedIdentity pins the published-context
223// shape rule at the terminal: only an Attached publication inherits a missing
224// handle or generation. A Picker or Connecting publication is recorded exactly
225// as stated, so an unattached capture never shows an old attachment's handle or
226// a generation no attachment committed.
227func TestTerminalUnattachedPublicationKeepsStatedIdentity(t *testing.T) {
228	terminal := newTestTerminal(t, 4, 2)
229
230	// Establish an attached context with a real handle and generation.
231	if err := terminal.PublishContext(ports.UIContext{AttachmentHandle: "attached-handle", Generation: 6, Status: ports.UIStatusAttached}); err != nil {
232		t.Fatal(err)
233	}
234
235	if err := terminal.PublishContext(ports.UIContext{AttachmentHandle: "picker-handle", Status: ports.UIStatusPicker}); err != nil {
236		t.Fatal(err)
237	}
238	snapshot, err := terminal.Snapshot()
239	if err != nil {
240		t.Fatal(err)
241	}
242	if snapshot.Context.AttachmentHandle != "picker-handle" || snapshot.Context.Generation != 0 || snapshot.Context.Status != ports.UIStatusPicker {
243		t.Fatalf("picker context = %#v", snapshot.Context)
244	}
245
246	// An Attached publication with a missing handle and generation still inherits
247	// them, exactly as the attachment foreground relies on.
248	if err := terminal.PublishContext(ports.UIContext{Status: ports.UIStatusAttached, OutputState: 3}); err != nil {
249		t.Fatal(err)
250	}
251	snapshot, err = terminal.Snapshot()
252	if err != nil {
253		t.Fatal(err)
254	}
255	if snapshot.Context.AttachmentHandle != "picker-handle" || snapshot.Context.Generation != 0 {
256		t.Fatalf("attached continuity context = %#v", snapshot.Context)
257	}
258}

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