v0.6.0 43c5a7c

runtime_test.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"testing"
6	"time"
7)
8
9func waitEntered(t *testing.T, entered <-chan struct{}) {
10	t.Helper()
11	select {
12	case <-entered:
13	case <-time.After(time.Second):
14		t.Fatal("view did not start")
15	}
16}
17
18func TestIdentityAndEvents(t *testing.T) {
19	r := newRuntime()
20	order := []string{"a", "b"}
21	var received []string
22	view := func(f *Frame) {
23		root := f.Root()
24		for _, key := range order {
25			if root.Button(key, Key(key)).Activated() {
26				received = append(received, key)
27			}
28		}
29	}
30	r.Build(view)
31	id := r.Target(1)
32	order = []string{"b", "a"}
33	r.Queue(inputEvent{Target: id, Kind: "activate"})
34	r.Queue(inputEvent{Target: id, Kind: "activate"})
35	if err := r.Wait(context.Background()); err != nil {
36		t.Fatal(err)
37	}
38	builds := 0
39	r.Build(func(f *Frame) { builds++; view(f) })
40	if builds != 1 || len(received) != 1 || received[0] != "b" {
41		t.Fatalf("batch: %v builds %d target=%+v first=%+v", received, builds, id, r.Target(0))
42	}
43	r.Redraw()
44	r.Build(view)
45	if len(received) != 1 {
46		t.Fatalf("expired event: %v", received)
47	}
48}
49func TestImplicitIdentity(t *testing.T) {
50	r := newRuntime()
51	r.Build(func(f *Frame) { f.Root().Box().Text("x") })
52	first := r.Target(0)
53	r.Redraw()
54	r.Build(func(f *Frame) { f.Root().Box().Text("y") })
55	if !first.same(r.Target(0)) {
56		t.Fatal("position/type identity changed")
57	}
58	r.Redraw()
59	r.Build(func(f *Frame) {
60		f.Root().Row()
61		if debugDiagnostics && len(f.Diagnostics()) != 1 {
62			t.Fatalf("missing unkeyed change diagnostic: %v", f.Diagnostics())
63		}
64	})
65	if first.same(r.Target(0)) {
66		t.Fatal("type omitted from identity")
67	}
68}
69func TestStaleAndDuplicates(t *testing.T) {
70	r := newRuntime()
71	var old Node
72	r.Build(func(f *Frame) {
73		old = f.Root()
74		old.Box(Key("dup"))
75		old.Box(Key("dup"))
76		if debugDiagnostics && len(f.Diagnostics()) != 1 {
77			t.Fatal(f.Diagnostics())
78		}
79	})
80	if old.valid() {
81		t.Fatal("stale node accepted")
82	}
83	if debugDiagnostics {
84		defer func() {
85			if recover() == nil {
86				t.Error("missing stale panic")
87			}
88		}()
89		old.Box()
90	}
91}
92func TestWake(t *testing.T) {
93	r := newRuntime()
94	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
95	defer cancel()
96	r.Redraw()
97	if err := r.Wait(ctx); err != nil {
98		t.Fatal(err)
99	}
100}
101
102func TestConcurrentBuildDefersCommit(t *testing.T) {
103	r := newRuntime()
104	entered := make(chan struct{})
105	release := make(chan struct{})
106	firstDone := make(chan bool, 1)
107	secondDone := make(chan bool, 1)
108	go func() { firstDone <- r.Build(func(f *Frame) { f.Root().Box(Key("first")); close(entered); <-release }) }()
109	waitEntered(t, entered)
110	go func() {
111		secondDone <- r.Build(func(f *Frame) { t.Error("busy view executed"); f.Root().Box(Key("second")) })
112	}()
113	select {
114	case built := <-secondDone:
115		if built {
116			t.Fatal("busy build committed")
117		}
118	case <-time.After(time.Second):
119		t.Fatal("concurrent Build blocked")
120	}
121	if r.Target(0) != nil {
122		t.Fatal("uncommitted tree visible")
123	}
124	close(release)
125	select {
126	case built := <-firstDone:
127		if !built {
128			t.Fatal("first build skipped")
129		}
130	case <-time.After(time.Second):
131		t.Fatal("first build did not finish")
132	}
133	if id := r.Target(0); id == nil || id.key != "first" {
134		t.Fatalf("commit out of order: %+v", id)
135	}
136	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
137	defer cancel()
138	if err := r.Wait(ctx); err != nil {
139		t.Fatalf("lost wake: %v", err)
140	}
141	if !r.Build(func(f *Frame) { f.Root().Box(Key("second")) }) {
142		t.Fatal("redraw skipped")
143	}
144	if id := r.Target(0); id == nil || id.key != "second" {
145		t.Fatalf("follow-up commit missing: %+v", id)
146	}
147}
148
149func TestReentrantBuildSchedulesQueuedEvents(t *testing.T) {
150	r := newRuntime()
151	r.Build(func(f *Frame) { f.Root().Button("target", Key("target")) })
152	target := r.Target(0)
153	r.Redraw()
154	finished := make(chan bool, 1)
155	go func() {
156		finished <- r.Build(func(f *Frame) {
157			f.Root().Button("target", Key("target"))
158			r.Queue(inputEvent{Target: target, Kind: "activate"})
159			if r.Build(func(*Frame) { t.Error("reentrant view executed") }) {
160				t.Error("reentrant build committed")
161			}
162		})
163	}()
164	select {
165	case built := <-finished:
166		if !built {
167			t.Fatal("outer build skipped")
168		}
169	case <-time.After(time.Second):
170		t.Fatal("reentrant Build deadlocked")
171	}
172	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
173	defer cancel()
174	if err := r.Wait(ctx); err != nil {
175		t.Fatalf("follow-up wake lost: %v", err)
176	}
177	if !r.Build(func(f *Frame) {
178		if !f.Root().Button("target", Key("target")).Activated() {
179			t.Error("queued event not delivered")
180		}
181	}) {
182		t.Fatal("follow-up skipped")
183	}
184	assertOneSettleFrame(t, r, func(f *Frame) { f.Root().Button("target", Key("target")) })
185}
186
187// assertOneSettleFrame checks that an event batch schedules exactly one
188// event-free follow-up build, and that this build schedules nothing more.
189func assertOneSettleFrame(t *testing.T, r *runtime, view func(*Frame)) {
190	t.Helper()
191	settled := false
192	if !r.Build(func(f *Frame) {
193		settled = true
194		if len(f.events) != 0 {
195			t.Error("settle frame carried events")
196		}
197		view(f)
198	}) || !settled {
199		t.Fatal("no settle frame after events")
200	}
201	if r.Build(func(*Frame) { t.Error("extra view executed") }) {
202		t.Fatal("extra redraw after settle frame")
203	}
204}
205
206func TestQueueDuringConstruction(t *testing.T) {
207	r := newRuntime()
208	r.Build(func(f *Frame) { f.Root().Button("target", Key("target")) })
209	target := r.Target(0)
210	r.Queue(inputEvent{Target: target, Kind: "activate"})
211	entered := make(chan struct{})
212	release := make(chan struct{})
213	result := make(chan bool, 1)
214	go func() {
215		result <- r.Build(func(f *Frame) {
216			root := f.Root()
217			if !root.Button("target", Key("target")).Activated() {
218				t.Error("first batch missing")
219			}
220			close(entered)
221			<-release
222		})
223	}()
224	waitEntered(t, entered)
225	r.Queue(inputEvent{Target: target, Kind: "activate"})
226	r.Redraw()
227	close(release)
228	if !<-result {
229		t.Fatal("first batch skipped")
230	}
231	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
232	defer cancel()
233	if err := r.Wait(ctx); err != nil {
234		t.Fatalf("lost wake during build: %v", err)
235	}
236	if !r.Build(func(f *Frame) {
237		if !f.Root().Button("target", Key("target")).Activated() {
238			t.Error("event queued during construction lost")
239		}
240	}) {
241		t.Fatal("second batch skipped")
242	}
243	assertOneSettleFrame(t, r, func(f *Frame) { f.Root().Button("target", Key("target")) })
244}
245
246func TestCheckboxElementAndActivation(t *testing.T) {
247	r := newRuntime()
248	checked := false
249	build := func(f *Frame) ChangeEvent { root := f.Root(); return root.Checkbox("accept", &checked, Key("accept")) }
250	r.Build(func(f *Frame) {
251		if build(f).Changed() {
252			t.Fatal("spurious change")
253		}
254	})
255	if e := r.committed.children[0]; e.typ != "checkbox" || e.role != "checkbox" || e.text != "accept" {
256		t.Fatalf("wrong semantics: %+v", e)
257	}
258	r.Queue(inputEvent{Target: r.Target(0), Kind: "activate"})
259	r.Build(func(f *Frame) {
260		ev := build(f)
261		changed := ev.Changed()
262		if !changed || ev.Changed() != changed || !checked {
263			t.Fatal("checkbox activation missing")
264		}
265	})
266	if e := r.committed.children[0]; e.typ != "checkbox" || e.role != "checkbox" {
267		t.Fatalf("wrong semantics after activation: %+v", e)
268	}
269	r.Queue(inputEvent{Target: r.Target(0), Kind: "activate"})
270	r.Build(func(f *Frame) { f.Root().Checkbox("accept", &checked, Key("accept"), Disabled(true)) })
271	if !checked {
272		t.Fatal("disabled checkbox mutated model")
273	}
274}
275
276func TestHeadingLevel(t *testing.T) {
277	r := newRuntime()
278	r.Build(func(f *Frame) { f.Root().Header().Heading("title", Level(1), Class("logo")) })
279	e := r.committed.children[0].children[0]
280	if e.typ != "heading" || e.role != "heading" || e.level != 1 || e.text != "title" || len(e.classes) != 1 || e.classes[0] != "logo" {
281		t.Fatalf("heading metadata: %+v", e)
282	}
283	defer func() {
284		if recover() == nil {
285			t.Error("invalid level accepted")
286		}
287	}()
288	Level(7)
289}

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 1

Tags 12

↑↓ moveenter openesc close