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}