notice_render_test.go RawHistoryFolder

1package daemon
2
3import (
4	"strings"
5	"testing"
6	"time"
7
8	"github.com/stretchr/testify/require"
9
10	vt "github.com/bnema/vev-vt"
11	"github.com/bnema/vev/internal/domain"
12	"github.com/bnema/vev/internal/protocol"
13	"github.com/bnema/vev/internal/protocol/wire"
14)
15
16// replayOutputFrames decodes and writes each Output envelope’s terminal bytes
17// into screen, growing its shadow the same way a real client would.
18func replayOutputFrames(t *testing.T, screen *vt.Screen, frames []wire.Envelope) {
19	t.Helper()
20	for _, f := range frames {
21		out, ok := decodeServerMessage(t, f).(protocol.Output)
22		require.True(t, ok)
23		screen.Write(out.Data)
24	}
25}
26
27// drainAllFrames collects every frame already queued on sends without blocking.
28func drainAllFrames(sends chan wire.Envelope) []wire.Envelope {
29	var out []wire.Envelope
30	for {
31		select {
32		case f := <-sends:
33			out = append(out, f)
34		default:
35			return out
36		}
37	}
38}
39
40// runeColumn returns the rune-indexed column at which text first occurs in
41// row, or -1. Rows are exactly one rune per terminal column, so this must not
42// use strings.Index: box-drawing borders are multi-byte and would inflate a
43// byte offset past the real column.
44func runeColumn(row, text string) int {
45	runes, sub := []rune(row), []rune(text)
46	for i := 0; i+len(sub) <= len(runes); i++ {
47		match := true
48		for j, r := range sub {
49			if runes[i+j] != r {
50				match = false
51				break
52			}
53		}
54		if match {
55			return i
56		}
57	}
58	return -1
59}
60
61// containsTopRight reports whether text appears anywhere at or past minCol in
62// the frame's top half.
63func containsTopRight(rows []string, text string, minCol int) bool {
64	for y, row := range rows {
65		if y > len(rows)/2 {
66			break
67		}
68		if runeColumn(row, text) >= minCol {
69			return true
70		}
71	}
72	return false
73}
74
75// TestPaintComposesNoticeToastTopRightAndExpiresOnTTL drives an actual
76// notify -> showToast -> paint -> emit round trip and decodes the resulting
77// wire bytes through a terminal emulator, the same way a real client would,
78// so this proves the composed frame (not just internal toast state) carries
79// the message top-right, and that it disappears once the fake clock crosses
80// the toast's TTL.
81func TestPaintComposesNoticeToastTopRightAndExpiresOnTTL(t *testing.T) {
82	clk := newNoticeClock()
83	d, sess, ac, sends := newNoticeFixture(t, clk)
84	screen := vt.NewScreen(ac.geometry.Size.Cols, ac.geometry.Size.Rows)
85
86	d.notify(sess, domain.NoticeError, domain.NoticePaneSpawn, "couldn't open pane", nil)
87	awaitToastCount(t, ac, 1)
88	replayOutputFrames(t, screen, drainAllFrames(sends))
89
90	rows := frameRows(captureTestFrame(screen))
91	if !containsTopRight(rows, "couldn't open pane", ac.geometry.Size.Cols/2) {
92		t.Fatalf("composed frame missing toast message top-right:\n%s", strings.Join(rows, "\n"))
93	}
94
95	clk.advance(noticeTTL(domain.NoticeError))
96	awaitToastCount(t, ac, 0)
97	select {
98	case f := <-sends:
99		replayOutputFrames(t, screen, append([]wire.Envelope{f}, drainAllFrames(sends)...))
100	case <-time.After(2 * time.Second):
101		t.Fatal("expiry did not invalidate the render")
102	}
103
104	rows = frameRows(captureTestFrame(screen))
105	if containsTopRight(rows, "couldn't open pane", ac.geometry.Size.Cols/2) {
106		t.Fatalf("toast message still present after TTL expired:\n%s", strings.Join(rows, "\n"))
107	}
108}

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