ui_publication_test.go RawHistoryFolder

1package daemon
2
3import (
4	"errors"
5	"testing"
6
7	renderer "github.com/bnema/vev-vt/ansi"
8	"github.com/bnema/vev/internal/adapters/sessionwire"
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/protocol"
11	"github.com/stretchr/testify/require"
12)
13
14func publicationTestContext() protocol.ViewContext {
15	return protocol.ViewContext{
16		Route: protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}},
17		TabID: "tab-1", FocusedPaneID: "pane-1",
18	}
19}
20
21func TestPreparedOutputSemanticPublication(t *testing.T) {
22	for _, scenario := range []string{"success", "send failure", "stale", "exhausted"} {
23		t.Run(scenario, func(t *testing.T) {
24			stream := newOutputStateStream()
25			frame := renderer.NewFrame(3, 1)
26			prepared, err := stream.prepare(frame, nil, true)
27			require.NoError(t, err)
28			context := publicationTestContext()
29			prepared.context = &context
30			if scenario == "stale" {
31				stream.rebase()
32			}
33			if scenario == "exhausted" {
34				stream.viewPublication = ^uint64(0)
35			}
36			before := stream.viewPublication
37			called := false
38			sendErr := errors.New("send failed")
39			err = prepared.send(prepared.data, 9, func(output protocol.Output) error {
40				called = true
41				require.NotNil(t, output.Context)
42				require.Equal(t, before+1, output.Context.Publication)
43				require.Equal(t, context.Route, output.Context.Route)
44				require.Equal(t, uint64(9), output.Echo)
45				_, marshalErr := sessionwire.EncodeServerMessage(output)
46				require.NoError(t, marshalErr)
47				if scenario == "send failure" {
48					return sendErr
49				}
50				return nil
51			})
52			switch scenario {
53			case "success":
54				require.NoError(t, err)
55				require.True(t, called)
56				require.True(t, prepared.sent)
57				require.Equal(t, before+1, stream.viewPublication)
58				sideEffect, err := stream.sideEffect([]byte("query"), 9)
59				require.NoError(t, err)
60				require.Nil(t, sideEffect.Context)
61				require.Equal(t, context.Route, stream.lastViewContext.Route)
62				boundary := prepared.boundary
63				require.Equal(t, protocol.UIReceipt{Epoch: stream.epoch, State: stream.next, ViewPublication: stream.viewPublication, Outcome: protocol.UIReceiptProcessed}, boundary)
64				stream.rebase()
65				require.Equal(t, boundary, prepared.boundary, "later view rebases cannot change an accepted receipt boundary")
66			case "send failure":
67				require.ErrorIs(t, err, sendErr)
68				require.True(t, called)
69				require.False(t, prepared.sent)
70				require.Equal(t, before, stream.viewPublication)
71				require.Zero(t, prepared.boundary)
72			case "stale", "exhausted":
73				if scenario == "exhausted" {
74					require.Error(t, err)
75				} else {
76					require.NoError(t, err)
77				}
78				require.False(t, called)
79				require.False(t, prepared.sent)
80				require.Equal(t, before, stream.viewPublication)
81			}
82		})
83	}
84}
85
86func TestPreparedOutputNoBytePublication(t *testing.T) {
87	for _, scenario := range []string{"unchanged", "fence", "focus", "failure", "stale"} {
88		t.Run(scenario, func(t *testing.T) {
89			stream := newOutputStateStream()
90			frame := renderer.NewFrame(3, 1)
91			context := publicationTestContext()
92			first, err := stream.prepare(frame, nil, true)
93			require.NoError(t, err)
94			first.context = &context
95			require.NoError(t, first.send(first.data, 0, func(protocol.Output) error { return nil }))
96			prepared, err := stream.prepare(frame, nil, false)
97			require.NoError(t, err)
98			require.Empty(t, prepared.data)
99			prepared.context = &context
100			if scenario == "focus" {
101				context.FocusedPaneID = "pane-2"
102			}
103			if scenario == "stale" {
104				stream.rebase()
105			}
106			epoch, state, outstanding := stream.epoch, stream.next, stream.outstanding()
107			called := false
108			sendErr := errors.New("metadata send failed")
109			err = prepared.publishNoBytes(scenario == "fence" || scenario == "failure" || scenario == "stale", func(update protocol.UIViewUpdate) error {
110				called = true
111				require.Equal(t, epoch, update.Epoch)
112				require.Equal(t, state, update.State)
113				require.Equal(t, uint64(2), update.Context.Publication)
114				require.Equal(t, context.FocusedPaneID, update.Context.FocusedPaneID)
115				_, marshalErr := sessionwire.EncodeServerMessage(update)
116				require.NoError(t, marshalErr)
117				if scenario == "failure" {
118					return sendErr
119				}
120				return nil
121			})
122			if scenario == "failure" {
123				require.ErrorIs(t, err, sendErr)
124			} else {
125				require.NoError(t, err)
126			}
127			require.Equal(t, scenario != "unchanged" && scenario != "stale", called)
128			require.Equal(t, scenario != "failure" && scenario != "stale", prepared.sent)
129			wantPublication := uint64(1)
130			if scenario == "focus" || scenario == "fence" {
131				wantPublication = 2
132			}
133			require.Equal(t, wantPublication, stream.viewPublication)
134			require.Equal(t, epoch, stream.epoch)
135			require.Equal(t, state, stream.next)
136			require.Equal(t, outstanding, stream.outstanding())
137			if scenario == "focus" || scenario == "fence" {
138				boundary := prepared.boundary
139				require.Equal(t, protocol.UIReceipt{Epoch: epoch, State: state, ViewPublication: wantPublication, Outcome: protocol.UIReceiptProcessed}, boundary)
140				stream.rebase()
141				require.Equal(t, boundary, prepared.boundary)
142			} else {
143				require.Zero(t, prepared.boundary, "only an accepted publication supplies a receipt boundary")
144			}
145		})
146	}
147}
148
149func TestCapturedViewContextUsesCapturedFocus(t *testing.T) {
150	for _, floating := range []bool{false, true} {
151		t.Run(map[bool]string{false: "tiled", true: "floating"}[floating], func(t *testing.T) {
152			context := publicationTestContext()
153			state := capturedRenderState{
154				route: context.Route,
155				view:  attachmentView{tabID: context.TabID, paneID: "not-the-rendered-focus"},
156				panes: []capturedPaneRenderState{
157					{stableID: "other-pane"},
158					{stableID: context.FocusedPaneID, focused: true},
159				},
160				floating: capturedFloatingRenderState{visible: floating, focused: floating, pane: capturedPaneRenderState{stableID: "floating-pane"}},
161			}
162			got := state.viewContext()
163			if floating {
164				context.FocusedPaneID = "floating-pane"
165			}
166			require.Equal(t, context, got)
167			state.route.Target.SessionName = "renamed"
168			state.panes[1].stableID = "replacement"
169			require.Equal(t, context, got, "published values do not alias capture storage")
170		})
171	}
172}

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