v0.33.0 62a09f5

content_reveal_test.go RawHistoryFolder

1package content
2
3import (
4	"context"
5	"testing"
6
7	"github.com/stretchr/testify/assert"
8	"github.com/stretchr/testify/mock"
9
10	"github.com/bnema/dumber/internal/application/port"
11	"github.com/bnema/dumber/internal/application/port/mocks"
12	"github.com/bnema/dumber/internal/domain/entity"
13	"github.com/bnema/dumber/internal/ui/component"
14)
15
16func newRevealTestCoordinator() *Coordinator {
17	return &Coordinator{
18		webViews:       make(map[entity.PaneID]port.WebView),
19		webViewPaneIDs: make(map[port.WebViewID]entity.PaneID),
20		paneTitles:     make(map[entity.PaneID]string),
21		navOrigins:     make(map[entity.PaneID]string),
22	}
23}
24
25func revealTestWebView(t *testing.T, id port.WebViewID, generation uint64) *mocks.MockWebView {
26	t.Helper()
27	wv := mocks.NewMockWebView(t)
28	wv.EXPECT().ID().Return(id).Maybe()
29	wv.EXPECT().Generation().Return(generation).Maybe()
30	return wv
31}
32
33func TestRevealState_IsBoundToCurrentWebViewAcrossReplacementPaths(t *testing.T) {
34	paneID := entity.PaneID("pane-1")
35	first := revealTestWebView(t, 101, 1)
36	// The pool may reuse the same native WebView ID; its reuse generation is
37	// part of the presentation identity.
38	directReplacement := revealTestWebView(t, 101, 2)
39	popupReplacement := revealTestWebView(t, 103, 1)
40	c := newRevealTestCoordinator()
41
42	c.setWebViewLocked(paneID, first)
43	firstIdentity, _ := identityForWebView(first)
44	c.markWebViewRevealed(paneID, firstIdentity)
45	assert.True(t, c.WebViewRevealed(paneID))
46
47	// A direct replacement for the same pane must show the fresh loading state.
48	c.setWebViewLocked(paneID, directReplacement)
49	assert.False(t, c.WebViewRevealed(paneID))
50
51	directReplacementIdentity, _ := identityForWebView(directReplacement)
52	c.markWebViewRevealed(paneID, directReplacementIdentity)
53	c.RegisterPopupWebView(paneID, popupReplacement)
54	assert.False(t, c.WebViewRevealed(paneID), "popup registration must not inherit a prior WebView reveal")
55}
56
57func TestRevealState_StoresIdentityAsTheOnlyStateValue(t *testing.T) {
58	paneID := entity.PaneID("pane-1")
59	wv := revealTestWebView(t, 101, 1)
60	c := newRevealTestCoordinator()
61	c.setWebViewLocked(paneID, wv)
62	identity, _ := identityForWebView(wv)
63
64	c.markPendingReveal(paneID, identity)
65	c.markWebViewRevealed(paneID, identity)
66
67	c.revealMu.Lock()
68	defer c.revealMu.Unlock()
69	assert.Equal(t, identity, c.pendingReveal[paneID])
70	assert.Equal(t, identity, c.revealedWebViews[paneID])
71}
72
73func TestRevealState_ReleaseClearsStateWithoutWebViewMapping(t *testing.T) {
74	paneID := entity.PaneID("pane-1")
75	wv := revealTestWebView(t, 101, 1)
76	c := newRevealTestCoordinator()
77	c.setWebViewLocked(paneID, wv)
78	identity, _ := identityForWebView(wv)
79	c.markWebViewRevealed(paneID, identity)
80
81	// Model an earlier partial cleanup that lost the WebView mapping. Release
82	// remains responsible for clearing presentation state on its early return.
83	c.webViewsMu.Lock()
84	delete(c.webViews, paneID)
85	c.webViewsMu.Unlock()
86	c.ReleaseWebView(context.Background(), paneID)
87
88	c.revealMu.Lock()
89	_, retained := c.revealedWebViews[paneID]
90	c.revealMu.Unlock()
91	assert.False(t, retained)
92}
93
94func TestRevealState_ReleaseMappedWebViewClearsRevealStateAndReturnsToPool(t *testing.T) {
95	paneID := entity.PaneID("pane-1")
96	wv := revealTestWebView(t, 101, 1)
97	pool := mocks.NewMockWebViewPool(t)
98	pool.EXPECT().Release(wv).Once()
99	c := newRevealTestCoordinator()
100	c.pool = pool
101	c.setWebViewLocked(paneID, wv)
102	identity, _ := identityForWebView(wv)
103	c.markWebViewRevealed(paneID, identity)
104
105	c.ReleaseWebView(context.Background(), paneID)
106
107	assert.Nil(t, c.GetWebView(paneID))
108	assert.False(t, c.WebViewRevealed(paneID))
109}
110
111func TestRevealState_StaleCallbackCannotRevealReplacement(t *testing.T) {
112	paneID := entity.PaneID("pane-1")
113	oldWV := revealTestWebView(t, 101, 1)
114	newWV := revealTestWebView(t, 102, 1)
115	oldWV.EXPECT().IsDestroyed().Return(false).Maybe()
116	c := newRevealTestCoordinator()
117	c.setWebViewLocked(paneID, oldWV)
118	oldIdentity, _ := identityForWebView(oldWV)
119	c.markPendingReveal(paneID, oldIdentity)
120	c.setWebViewLocked(paneID, newWV)
121
122	c.revealIfPending(context.Background(), paneID, oldWV, oldIdentity, "", "stale")
123	assert.False(t, c.WebViewRevealed(paneID))
124}
125
126func TestRevealState_RevealedCurrentWebViewPersistsForWorkspaceRebuild(t *testing.T) {
127	paneID := entity.PaneID("pane-1")
128	wv := revealTestWebView(t, 101, 1)
129	c := newRevealTestCoordinator()
130	c.setWebViewLocked(paneID, wv)
131	identity, _ := identityForWebView(wv)
132	c.markWebViewRevealed(paneID, identity)
133
134	// AttachToWorkspace asks this identity-aware query when rebuilding a pane,
135	// so the replacement PaneView hides its skeleton for this same WebView.
136	assert.True(t, c.webViewRevealed(paneID, wv))
137
138	replacement := revealTestWebView(t, 102, 1)
139	assert.False(t, c.webViewRevealed(paneID, replacement))
140}
141
142func TestRevealCallback_CapturesGenerationBeforeSameObjectPoolReuse(t *testing.T) {
143	paneID := entity.PaneID("pane-1")
144	wv := mocks.NewMockWebView(t)
145	generation := uint64(1)
146	wv.EXPECT().ID().Return(port.WebViewID(101)).Maybe()
147	wv.EXPECT().Generation().RunAndReturn(func() uint64 { return generation }).Maybe()
148	wv.EXPECT().IsDestroyed().Return(false).Maybe()
149	var callbacks *port.WebViewCallbacks
150	wv.EXPECT().SetCallbacks(mock.Anything).RunAndReturn(func(value *port.WebViewCallbacks) {
151		callbacks = value
152	}).Once()
153
154	c := newRevealTestCoordinator()
155	c.getActiveWS = func() (*entity.Workspace, *component.WorkspaceView) { return nil, nil }
156	c.setWebViewLocked(paneID, wv)
157	installedIdentity, _ := identityForWebView(wv)
158	c.markPendingReveal(paneID, installedIdentity)
159	c.setupWebViewCallbacks(context.Background(), paneID, wv)
160	revealed := false
161	c.setWebViewVisible = func(port.WebView) { revealed = true }
162
163	// The callback was installed at generation 1. A pool reuse can advance the
164	// same Go WebView object before its old callback is delivered.
165	generation = 2
166	callbacks.OnProgressChanged(1)
167
168	assert.False(t, revealed, "the generation-1 callback must not reveal generation 2")
169	assert.False(t, c.WebViewRevealed(paneID))
170}
171
172func TestRevealIfPending_SerializesReplacementUntilNativeVisibility(t *testing.T) {
173	paneID := entity.PaneID("pane-1")
174	oldWV := revealTestWebView(t, 101, 1)
175	oldWV.EXPECT().IsDestroyed().Return(false).Once()
176	newWV := revealTestWebView(t, 102, 1)
177	c := newRevealTestCoordinator()
178	c.setWebViewLocked(paneID, oldWV)
179	identity, _ := identityForWebView(oldWV)
180	c.markPendingReveal(paneID, identity)
181
182	nativeStarted := make(chan struct{})
183	allowNativeReturn := make(chan struct{})
184	c.setWebViewVisible = func(wv port.WebView) {
185		assert.Same(t, oldWV, wv)
186		close(nativeStarted)
187		<-allowNativeReturn
188	}
189	revealed := make(chan struct{})
190	go func() {
191		c.revealIfPending(context.Background(), paneID, oldWV, identity, "", "test")
192		close(revealed)
193	}()
194	<-nativeStarted
195
196	replaced := make(chan struct{})
197	go func() {
198		c.setWebViewLocked(paneID, newWV)
199		close(replaced)
200	}()
201	select {
202	case <-replaced:
203		t.Fatal("replacement interleaved before native visibility completed")
204	default:
205	}
206
207	close(allowNativeReturn)
208	<-revealed
209	<-replaced
210	assert.Same(t, newWV, c.GetWebView(paneID))
211	assert.False(t, c.WebViewRevealed(paneID), "replacement must not inherit old reveal state")
212}

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 8

Tags 42

↑↓ moveenter openesc close