1package content
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/mock"
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/dumber/internal/application/port"
13 "github.com/bnema/dumber/internal/application/port/mocks"
14 "github.com/bnema/dumber/internal/domain/entity"
15)
16
17// newMinimalCoordinator returns a Coordinator with the maps required by
18// ReleaseWebView initialized so that calls to clearPendingAppearance,
19// titleMu / navOriginMu locks do not panic.
20func newMinimalCoordinator() *Coordinator {
21 return &Coordinator{
22 webViews: make(map[entity.PaneID]port.WebView),
23 paneTitles: make(map[entity.PaneID]string),
24 navOrigins: make(map[entity.PaneID]string),
25 }
26}
27
28// ---------------------------------------------------------------------------
29// EnsureWebView
30// ---------------------------------------------------------------------------
31
32func TestLifecycle_EnsureWebView_ReusesExistingNonDestroyed(t *testing.T) {
33 t.Parallel()
34
35 wv := mocks.NewMockWebView(t)
36 wv.EXPECT().IsDestroyed().Return(false)
37
38 pool := mocks.NewMockWebViewPool(t)
39 // pool.Acquire must NOT be called
40
41 c := newMinimalCoordinator()
42 c.pool = pool
43 c.webViews[entity.PaneID("pane-1")] = wv
44
45 got, err := c.EnsureWebView(context.Background(), "pane-1")
46
47 require.NoError(t, err)
48 assert.Equal(t, wv, got)
49}
50
51func TestLifecycle_EnsureWebView_AcquiresFromPoolWhenNoneExists(t *testing.T) {
52 t.Parallel()
53
54 newWV := mocks.NewMockWebView(t)
55 newWV.EXPECT().SetCallbacks(mock.Anything).Maybe()
56 newWV.EXPECT().Generation().Return(uint64(0)).Maybe()
57
58 pool := mocks.NewMockWebViewPool(t)
59 pool.EXPECT().Acquire(mock.Anything).Return(newWV, nil)
60
61 c := newMinimalCoordinator()
62 c.pool = pool
63
64 got, err := c.EnsureWebView(context.Background(), "pane-1")
65
66 require.NoError(t, err)
67 assert.Equal(t, newWV, got)
68 assert.Equal(t, newWV, c.webViews[entity.PaneID("pane-1")])
69}
70
71func TestLifecycle_EnsureWebView_AcquiresFromPoolWhenExistingIsDestroyed(t *testing.T) {
72 t.Parallel()
73
74 oldWV := mocks.NewMockWebView(t)
75 oldWV.EXPECT().IsDestroyed().Return(true)
76
77 newWV := mocks.NewMockWebView(t)
78 newWV.EXPECT().SetCallbacks(mock.Anything).Maybe()
79 newWV.EXPECT().Generation().Return(uint64(0)).Maybe()
80
81 pool := mocks.NewMockWebViewPool(t)
82 pool.EXPECT().Acquire(mock.Anything).Return(newWV, nil)
83
84 c := newMinimalCoordinator()
85 c.pool = pool
86 c.webViews[entity.PaneID("pane-1")] = oldWV
87
88 got, err := c.EnsureWebView(context.Background(), "pane-1")
89
90 require.NoError(t, err)
91 assert.Equal(t, newWV, got)
92}
93
94func TestLifecycle_EnsureWebView_ErrorWhenPoolIsNil(t *testing.T) {
95 t.Parallel()
96
97 c := newMinimalCoordinator()
98 // pool remains nil
99
100 _, err := c.EnsureWebView(context.Background(), "pane-1")
101
102 require.Error(t, err)
103 assert.Contains(t, err.Error(), "webview pool not configured")
104}
105
106func TestLifecycle_EnsureWebView_ErrorWhenAcquireFails(t *testing.T) {
107 t.Parallel()
108
109 acquireErr := errors.New("pool exhausted")
110
111 pool := mocks.NewMockWebViewPool(t)
112 pool.EXPECT().Acquire(mock.Anything).Return(nil, acquireErr)
113
114 c := newMinimalCoordinator()
115 c.pool = pool
116
117 _, err := c.EnsureWebView(context.Background(), "pane-1")
118
119 require.Error(t, err)
120 assert.Equal(t, acquireErr, err)
121}
122
123// ---------------------------------------------------------------------------
124// ReleaseWebView
125// ---------------------------------------------------------------------------
126
127func TestLifecycle_ReleaseWebView_ReleasesToPool(t *testing.T) {
128 t.Parallel()
129
130 wv := mocks.NewMockWebView(t)
131 // idleInhibitor is nil so these won't be called
132 wv.EXPECT().IsFullscreen().Return(false).Maybe()
133 wv.EXPECT().IsPlayingAudio().Return(false).Maybe()
134
135 pool := mocks.NewMockWebViewPool(t)
136 pool.EXPECT().Release(wv)
137
138 c := newMinimalCoordinator()
139 c.pool = pool
140 c.webViews[entity.PaneID("pane-1")] = wv
141
142 c.ReleaseWebView(context.Background(), "pane-1")
143
144 _, exists := c.webViews[entity.PaneID("pane-1")]
145 assert.False(t, exists, "webview should be removed from map after release")
146}
147
148func TestLifecycle_ReleaseWebView_NoopForUnknownPane(t *testing.T) {
149 t.Parallel()
150
151 pool := mocks.NewMockWebViewPool(t)
152 // pool.Release must NOT be called
153
154 c := newMinimalCoordinator()
155 c.pool = pool
156
157 // Should not panic
158 c.ReleaseWebView(context.Background(), "unknown-pane")
159}
160
161func TestLifecycle_ReleaseWebView_UninhibitsIdleIfFullscreen(t *testing.T) {
162 t.Parallel()
163
164 wv := mocks.NewMockWebView(t)
165 wv.EXPECT().IsFullscreen().Return(true)
166 wv.EXPECT().IsPlayingAudio().Return(false)
167
168 inhibitor := mocks.NewMockIdleInhibitor(t)
169 inhibitor.EXPECT().Uninhibit(mock.Anything).Return(nil)
170
171 pool := mocks.NewMockWebViewPool(t)
172 pool.EXPECT().Release(wv)
173
174 c := newMinimalCoordinator()
175 c.pool = pool
176 c.idleInhibitor = inhibitor
177 c.webViews[entity.PaneID("pane-1")] = wv
178
179 c.ReleaseWebView(context.Background(), "pane-1")
180}
181
182func TestLifecycle_ReleaseWebView_UninhibitsIdleIfPlayingAudio(t *testing.T) {
183 t.Parallel()
184
185 wv := mocks.NewMockWebView(t)
186 wv.EXPECT().IsFullscreen().Return(false)
187 wv.EXPECT().IsPlayingAudio().Return(true)
188
189 inhibitor := mocks.NewMockIdleInhibitor(t)
190 inhibitor.EXPECT().Uninhibit(mock.Anything).Return(nil)
191
192 pool := mocks.NewMockWebViewPool(t)
193 pool.EXPECT().Release(wv)
194
195 c := newMinimalCoordinator()
196 c.pool = pool
197 c.idleInhibitor = inhibitor
198 c.webViews[entity.PaneID("pane-1")] = wv
199
200 c.ReleaseWebView(context.Background(), "pane-1")
201}
202
203func TestLifecycle_ReleaseWebView_DestroysWebViewWhenPoolIsNil(t *testing.T) {
204 t.Parallel()
205
206 wv := mocks.NewMockWebView(t)
207 // idleInhibitor is nil so these won't be called
208 wv.EXPECT().IsFullscreen().Return(false).Maybe()
209 wv.EXPECT().IsPlayingAudio().Return(false).Maybe()
210 wv.EXPECT().Destroy()
211
212 c := newMinimalCoordinator()
213 // pool remains nil
214 c.webViews[entity.PaneID("pane-1")] = wv
215
216 c.ReleaseWebView(context.Background(), "pane-1")
217}
218
219// ---------------------------------------------------------------------------
220// GetWebView / RegisterPopupWebView
221// ---------------------------------------------------------------------------
222
223func TestLifecycle_GetWebView_ReturnsRegisteredWebView(t *testing.T) {
224 t.Parallel()
225
226 wv := mocks.NewMockWebView(t)
227
228 c := newMinimalCoordinator()
229 c.webViews[entity.PaneID("pane-1")] = wv
230
231 got := c.GetWebView("pane-1")
232
233 assert.Equal(t, wv, got)
234}
235
236func TestLifecycle_RegisterPopupWebView_StoresWebView(t *testing.T) {
237 t.Parallel()
238
239 wv := mocks.NewMockWebView(t)
240
241 c := newMinimalCoordinator()
242
243 c.RegisterPopupWebView("popup-1", wv)
244
245 assert.Equal(t, wv, c.GetWebView("popup-1"))
246}
247
248func TestLifecycle_RegisterPopupWebView_IgnoresNil(t *testing.T) {
249 t.Parallel()
250
251 c := newMinimalCoordinator()
252
253 // Should not panic
254 c.RegisterPopupWebView("popup-1", nil)
255
256 assert.Nil(t, c.GetWebView("popup-1"))
257}