v0.2.0 80c61e3

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

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