final_acceptance_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5
6	vt "github.com/bnema/vev-vt"
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/domain/layout"
9	"github.com/bnema/vev/internal/protocol"
10	"github.com/stretchr/testify/require"
11)
12
13func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *testing.T) {
14	d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY())
15	first.clientID[0] = 1
16	firstTab := sess.tabs[0]
17	firstTab.stableID = "tab-first"
18	firstTab.panes[firstTab.focusedPane().id].stableID = "pane-first"
19
20	secondTransport, _ := newCapturingTransport(t)
21	second := &attachedClient{
22		tr:       secondTransport,
23		output:   newOutputStateStream(),
24		geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
25	}
26	second.initOverlays()
27	second.clientID[0] = 2
28	second.setSession(sess)
29	secondTab := newTabWithStableID("tab-second", "pane-second", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
30	secondTab.ctx, secondTab.cancel = sess.ctx, func() {}
31	publishTiledPaneOwners(sess, secondTab)
32	require.NoError(t, sess.runMutation(func() error {
33		sess.mu.Lock()
34		defer sess.mu.Unlock()
35		sess.tabs = append(sess.tabs, secondTab)
36		return nil
37	}))
38	require.True(t, sess.registerAttachment(second))
39	require.True(t, sess.selectAttachmentTab(first, domain.TabStableID(firstTab.stableID)))
40	require.True(t, sess.selectAttachmentTab(second, domain.TabStableID(secondTab.stableID)))
41
42	d.ptys = newFactory(t, newQuietPTY())
43	rc := d.attachCoordinator(sess, nil, first, true)
44	d.attachCoordinator(sess, nil, second, true)
45	firstToken := sess.captureAttachmentCapability(first, first.transport())
46	firstToken.lease = rc.attachmentLease(first)
47	first.installTestAttachmentCapability(firstToken)
48	secondToken := sess.captureAttachmentCapability(second, second.transport())
49	secondToken.lease = rc.attachmentLease(second)
50	second.installTestAttachmentCapability(secondToken)
51
52	firstView := first.viewSnapshot()
53	secondView := second.viewSnapshot()
54	first.sendMu.Lock()
55	firstEpoch := first.output.currentEpoch()
56	first.sendMu.Unlock()
57	secondEffect, admitted := second.beginAttachmentEffect(secondToken)
58	require.True(t, admitted)
59	require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40}))
60	secondEffect.End()
61
62	d.enterPalette(sess, second)
63	d.handlePaletteInput(second, []byte("CNT\r"))
64
65	sess.mu.Lock()
66	tabs := append([]*tab(nil), sess.tabs...)
67	sess.mu.Unlock()
68	require.Len(t, tabs, 3)
69	wantContent := domain.Size{Cols: 120, Rows: 38}
70	for _, tb := range tabs {
71		tb.mu.Lock()
72		gotTabSize := tb.size
73		pane := tb.focusedPane()
74		pane.mu.Lock()
75		gotPaneSize := domain.Size{Cols: pane.screen.Columns(), Rows: pane.screen.Rows()}
76		pane.mu.Unlock()
77		tb.mu.Unlock()
78		require.Equal(t, wantContent, gotTabSize)
79		require.Equal(t, wantContent, gotPaneSize)
80	}
81	require.Equal(t, firstView.tabID, first.viewSnapshot().tabID, "new tab selection crossed attachment boundary")
82	require.NotEqual(t, secondView.tabID, second.viewSnapshot().tabID, "palette did not select the created tab")
83	first.sendMu.Lock()
84	gotFirstEpoch := first.output.currentEpoch()
85	first.sendMu.Unlock()
86	require.Equal(t, firstEpoch, gotFirstEpoch, "new tab render crossed attachment boundary")
87	second.sendMu.Lock()
88	gotSecondSize := second.geometry.Size
89	second.sendMu.Unlock()
90	require.Equal(t, domain.Size{Cols: 120, Rows: 40}, gotSecondSize)
91}
92
93func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing.T) {
94	d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY())
95	first.clientID[0] = 1
96	firstTab := sess.tabs[0]
97	firstTab.stableID = "tab-first"
98	firstTab.panes[firstTab.focusedPane().id].stableID = "pane-first"
99
100	secondTransport, _ := newCapturingTransport(t)
101	second := &attachedClient{
102		tr:       secondTransport,
103		output:   newOutputStateStream(),
104		geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
105	}
106	second.initOverlays()
107	second.clientID[0] = 2
108	second.setSession(sess)
109
110	secondTab := newTabWithStableID("tab-second", "pane-second", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
111	secondTab.ctx, secondTab.cancel = sess.ctx, func() {}
112	publishTiledPaneOwners(sess, secondTab)
113	require.NoError(t, sess.runMutation(func() error {
114		sess.mu.Lock()
115		defer sess.mu.Unlock()
116		sess.tabs = append(sess.tabs, secondTab)
117		return nil
118	}))
119	require.True(t, sess.registerAttachment(second))
120	require.True(t, sess.selectAttachmentTab(first, domain.TabStableID(firstTab.stableID)))
121	require.True(t, sess.selectAttachmentTab(second, domain.TabStableID(secondTab.stableID)))
122
123	firstBefore := first.viewSnapshot()
124	secondBefore := second.viewSnapshot()
125	require.True(t, sess.updateAttachmentView(first, func(view *attachmentView) {
126		view.windowTop = 7
127		view.windowRows = 12
128		view.bookmark = 11
129		view.liveBottom = false
130	}))
131	gotFirst := first.viewSnapshot()
132	gotSecond := second.viewSnapshot()
133	require.Equal(t, domain.TabStableID(firstTab.stableID), gotFirst.tabID)
134	require.Equal(t, 7, gotFirst.windowTop)
135	require.Equal(t, vt.RowID(11), gotFirst.bookmark)
136	require.False(t, gotFirst.liveBottom)
137	require.Equal(t, secondBefore, gotSecond, "first attachment view mutation leaked to peer")
138	require.NotEqual(t, firstBefore.revision, gotFirst.revision)
139
140	rc := d.attachCoordinator(sess, nil, first, true)
141	d.attachCoordinator(sess, nil, second, true)
142	firstToken := sess.captureAttachmentCapability(first, first.transport())
143	firstToken.lease = rc.attachmentLease(first)
144	first.installTestAttachmentCapability(firstToken)
145	secondToken := sess.captureAttachmentCapability(second, second.transport())
146	secondToken.lease = rc.attachmentLease(second)
147	second.installTestAttachmentCapability(secondToken)
148	firstEpoch := first.output.currentEpoch()
149	secondEpoch := second.output.currentEpoch()
150
151	d.handleAttachmentClientFrame(secondToken, mustClientEnvelope(protocol.OutputResetRequest{}))
152	require.Greater(t, second.output.currentEpoch(), secondEpoch)
153	require.Equal(t, firstEpoch, first.output.currentEpoch(), "peer output reset crossed attachment boundary")
154
155	secondEffect, admitted := second.beginAttachmentEffect(secondToken)
156	require.True(t, admitted)
157	require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40}))
158	secondEffect.End()
159	require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.geometry.Size)
160	require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.geometry.Size, "peer resize crossed attachment boundary")
161	sess.mu.Lock()
162	tb := sess.tabs[0]
163	sess.mu.Unlock()
164	tb.mu.Lock()
165	sharedSize := tb.size
166	tb.mu.Unlock()
167	require.Equal(t, domain.Size{Cols: 120, Rows: 38}, sharedSize, "latest attachment resize must update shared session content")
168
169	d.clientGone(sess, first, first.transport(), true)
170	attachments := sess.snapshotAttachments()
171	for _, candidate := range attachments {
172		require.NotSame(t, first, candidate, "detached attachment remained registered")
173	}
174	var peerRegistered bool
175	for _, candidate := range attachments {
176		if candidate == second {
177			peerRegistered = true
178			break
179		}
180	}
181	require.True(t, peerRegistered, "detaching one attachment removed its peer")
182	require.Same(t, sess, second.currentSession())
183}
184
185func TestAcceptanceDirectAttachmentSupportsVerticalSplit(t *testing.T) {
186	d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY())
187	d.ptys = newFactory(t, newQuietPTY())
188	rc := d.attachCoordinator(sess, nil, ac, true)
189	token := sess.captureAttachmentCapability(ac, ac.transport())
190	token.lease = rc.attachmentLease(ac)
191	ac.installTestAttachmentCapability(token)
192
193	d.enterPalette(sess, ac)
194	d.handlePaletteInput(ac, []byte("SPD\r"))
195
196	sess.mu.Lock()
197	tab := sess.tabs[0]
198	sess.mu.Unlock()
199	tab.mu.Lock()
200	defer tab.mu.Unlock()
201	require.NotNil(t, tab.tree)
202	require.NotNil(t, tab.tree.Root)
203	require.Equal(t, layout.Vertical, tab.tree.Root.Dir)
204	require.Len(t, tab.panes, 2)
205}

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