detached 964c849

test(browser): add popup lifecycle unit tests

Add comprehensive unit tests for popup lifecycle:
- pendingPopups map operations
- popup ID tracking in paneNode
- active popup children tracking
- OAuth auto-close URL detection
- popup behavior configuration
- parent-child relationship tracking
- cleanup generation tracking
- multiple pending popups handling
commit
964c8498224fe28ce95b3a6ed41316a8b6f7bced
author
bamen · · 2025-10-12 07:06 UTC
parent
9f96981
signature
unsigned
1 file · +419 -0
A internal/app/browser/workspace_popup_test.go +419 -0
@@ -0,0 +1,419 @@
1+package browser
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/dumber/internal/config"
7+ "github.com/bnema/dumber/pkg/webkit"
8+)
9+
10+// TestPendingPopupStorage tests the pendingPopups map operations
11+func TestPendingPopupStorage(t *testing.T) {
12+ wm := &WorkspaceManager{
13+ pendingPopups: make(map[uint64]*pendingPopup),
14+ }
15+
16+ // Test adding a pending popup
17+ popupID := uint64(1)
18+ pending := &pendingPopup{
19+ url: "https://example.com",
20+ }
21+
22+ wm.pendingPopups[popupID] = pending
23+
24+ // Verify storage
25+ retrieved, ok := wm.pendingPopups[popupID]
26+ if !ok {
27+ t.Errorf("Expected popup ID %d to be stored", popupID)
28+ }
29+
30+ if retrieved.url != "https://example.com" {
31+ t.Errorf("Expected URL 'https://example.com', got %s", retrieved.url)
32+ }
33+
34+ // Test deletion
35+ delete(wm.pendingPopups, popupID)
36+ _, ok = wm.pendingPopups[popupID]
37+ if ok {
38+ t.Errorf("Expected popup ID %d to be deleted", popupID)
39+ }
40+}
41+
42+// TestPendingPopupCleanup tests cleanup of pending popups that never showed
43+func TestPendingPopupCleanup(t *testing.T) {
44+ wm := &WorkspaceManager{
45+ pendingPopups: make(map[uint64]*pendingPopup),
46+ }
47+
48+ // Add multiple pending popups
49+ for i := uint64(1); i <= 5; i++ {
50+ wm.pendingPopups[i] = &pendingPopup{
51+ url: "https://example.com",
52+ }
53+ }
54+
55+ // Verify all added
56+ if len(wm.pendingPopups) != 5 {
57+ t.Errorf("Expected 5 pending popups, got %d", len(wm.pendingPopups))
58+ }
59+
60+ // Simulate close before ready-to-show for popup 3
61+ popupID := uint64(3)
62+ if _, ok := wm.pendingPopups[popupID]; ok {
63+ delete(wm.pendingPopups, popupID)
64+ }
65+
66+ // Verify cleanup
67+ if len(wm.pendingPopups) != 4 {
68+ t.Errorf("Expected 4 pending popups after cleanup, got %d", len(wm.pendingPopups))
69+ }
70+
71+ _, ok := wm.pendingPopups[popupID]
72+ if ok {
73+ t.Errorf("Expected popup ID %d to be removed", popupID)
74+ }
75+}
76+
77+// TestPopupIDTracking tests that popup IDs are tracked correctly in paneNode
78+func TestPopupIDTracking(t *testing.T) {
79+ node := &paneNode{
80+ isLeaf: true,
81+ windowType: webkit.WindowTypePopup,
82+ isPopup: true,
83+ popupID: 42,
84+ }
85+
86+ // Verify popup ID is stored
87+ if node.popupID != 42 {
88+ t.Errorf("Expected popupID 42, got %d", node.popupID)
89+ }
90+
91+ // Verify window type
92+ if node.windowType != webkit.WindowTypePopup {
93+ t.Errorf("Expected WindowTypePopup, got %d", node.windowType)
94+ }
95+
96+ // Verify popup flag
97+ if !node.isPopup {
98+ t.Errorf("Expected isPopup to be true")
99+ }
100+}
101+
102+// TestActivePopupChildren tests tracking of active popup children in parent pane
103+func TestActivePopupChildren(t *testing.T) {
104+ parentNode := &paneNode{
105+ isLeaf: true,
106+ activePopupChildren: make([]string, 0),
107+ }
108+
109+ // Add popup children
110+ popupID1 := "1"
111+ popupID2 := "2"
112+ popupID3 := "3"
113+
114+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupID1)
115+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupID2)
116+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupID3)
117+
118+ // Verify count
119+ if len(parentNode.activePopupChildren) != 3 {
120+ t.Errorf("Expected 3 active popup children, got %d", len(parentNode.activePopupChildren))
121+ }
122+
123+ // Test removal of middle child
124+ for i, childID := range parentNode.activePopupChildren {
125+ if childID == popupID2 {
126+ parentNode.activePopupChildren = append(
127+ parentNode.activePopupChildren[:i],
128+ parentNode.activePopupChildren[i+1:]...,
129+ )
130+ break
131+ }
132+ }
133+
134+ // Verify removal
135+ if len(parentNode.activePopupChildren) != 2 {
136+ t.Errorf("Expected 2 active popup children after removal, got %d", len(parentNode.activePopupChildren))
137+ }
138+
139+ // Verify correct children remain
140+ if parentNode.activePopupChildren[0] != popupID1 || parentNode.activePopupChildren[1] != popupID3 {
141+ t.Errorf("Expected children [%s, %s], got %v", popupID1, popupID3, parentNode.activePopupChildren)
142+ }
143+}
144+
145+// TestShouldAutoClose tests OAuth URL detection logic
146+func TestShouldAutoClose(t *testing.T) {
147+ tests := []struct {
148+ name string
149+ url string
150+ enabled bool
151+ expected bool
152+ }{
153+ {
154+ name: "OAuth authorize URL",
155+ url: "https://accounts.google.com/o/oauth2/v2/auth?client_id=...",
156+ enabled: true,
157+ expected: true,
158+ },
159+ {
160+ name: "OAuth callback URL",
161+ url: "https://example.com/auth/callback?code=xyz",
162+ enabled: true,
163+ expected: true,
164+ },
165+ {
166+ name: "OAuth token response",
167+ url: "https://example.com/redirect?access_token=xyz",
168+ enabled: true,
169+ expected: true,
170+ },
171+ {
172+ name: "Regular URL - auto-close enabled",
173+ url: "https://example.com/page",
174+ enabled: true,
175+ expected: false,
176+ },
177+ {
178+ name: "OAuth URL - auto-close disabled",
179+ url: "https://accounts.google.com/o/oauth2/v2/auth?client_id=...",
180+ enabled: false,
181+ expected: false,
182+ },
183+ {
184+ name: "OpenID Connect URL",
185+ url: "https://example.com/oidc/authorize",
186+ enabled: true,
187+ expected: true,
188+ },
189+ {
190+ name: "Error response",
191+ url: "https://example.com/callback?error=access_denied",
192+ enabled: true,
193+ expected: true,
194+ },
195+ }
196+
197+ for _, tt := range tests {
198+ t.Run(tt.name, func(t *testing.T) {
199+ wm := &WorkspaceManager{
200+ app: &BrowserApp{
201+ config: &config.Config{
202+ Workspace: config.WorkspaceConfig{
203+ Popups: config.PopupBehaviorConfig{
204+ OAuthAutoClose: tt.enabled,
205+ },
206+ },
207+ },
208+ },
209+ }
210+
211+ result := wm.shouldAutoClose(tt.url)
212+ if result != tt.expected {
213+ t.Errorf("shouldAutoClose(%q) with enabled=%v = %v, want %v",
214+ tt.url, tt.enabled, result, tt.expected)
215+ }
216+ })
217+ }
218+}
219+
220+// TestPopupBehaviorConfig tests popup behavior configuration
221+func TestPopupBehaviorConfig(t *testing.T) {
222+ tests := []struct {
223+ name string
224+ behavior config.PopupBehavior
225+ }{
226+ {"Split behavior", config.PopupBehaviorSplit},
227+ {"Stacked behavior", config.PopupBehaviorStacked},
228+ {"Tabbed behavior", config.PopupBehaviorTabbed},
229+ {"Windowed behavior", config.PopupBehaviorWindowed},
230+ }
231+
232+ for _, tt := range tests {
233+ t.Run(tt.name, func(t *testing.T) {
234+ cfg := &config.Config{
235+ Workspace: config.WorkspaceConfig{
236+ Popups: config.PopupBehaviorConfig{
237+ Behavior: tt.behavior,
238+ },
239+ },
240+ }
241+
242+ // Verify behavior is stored correctly
243+ if cfg.Workspace.Popups.Behavior != tt.behavior {
244+ t.Errorf("Expected behavior %s, got %s",
245+ tt.behavior, cfg.Workspace.Popups.Behavior)
246+ }
247+ })
248+ }
249+}
250+
251+// TestPopupWindowTypeTracking tests window type tracking in paneNode
252+func TestPopupWindowTypeTracking(t *testing.T) {
253+ tests := []struct {
254+ name string
255+ windowType webkit.WindowType
256+ isPopup bool
257+ isRelated bool
258+ }{
259+ {
260+ name: "Regular tab",
261+ windowType: webkit.WindowTypeTab,
262+ isPopup: false,
263+ isRelated: false,
264+ },
265+ {
266+ name: "Popup window",
267+ windowType: webkit.WindowTypePopup,
268+ isPopup: true,
269+ isRelated: true,
270+ },
271+ }
272+
273+ for _, tt := range tests {
274+ t.Run(tt.name, func(t *testing.T) {
275+ node := &paneNode{
276+ windowType: tt.windowType,
277+ isPopup: tt.isPopup,
278+ isRelated: tt.isRelated,
279+ }
280+
281+ if node.windowType != tt.windowType {
282+ t.Errorf("Expected windowType %d, got %d", tt.windowType, node.windowType)
283+ }
284+
285+ if node.isPopup != tt.isPopup {
286+ t.Errorf("Expected isPopup %v, got %v", tt.isPopup, node.isPopup)
287+ }
288+
289+ if node.isRelated != tt.isRelated {
290+ t.Errorf("Expected isRelated %v, got %v", tt.isRelated, node.isRelated)
291+ }
292+ })
293+ }
294+}
295+
296+// TestPopupParentRelationship tests parent-child relationship tracking
297+func TestPopupParentRelationship(t *testing.T) {
298+ parentNode := &paneNode{
299+ isLeaf: true,
300+ activePopupChildren: make([]string, 0),
301+ }
302+
303+ popupNode := &paneNode{
304+ isLeaf: true,
305+ windowType: webkit.WindowTypePopup,
306+ isPopup: true,
307+ parentPane: parentNode,
308+ isRelated: true,
309+ }
310+
311+ // Verify parent relationship
312+ if popupNode.parentPane != parentNode {
313+ t.Errorf("Expected popup's parent to be parentNode")
314+ }
315+
316+ // Verify popup is marked as related
317+ if !popupNode.isRelated {
318+ t.Errorf("Expected popup to be marked as related")
319+ }
320+
321+ // Add popup to parent's children
322+ popupWebViewID := "test-popup-id"
323+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupWebViewID)
324+
325+ // Verify parent tracks popup
326+ if len(parentNode.activePopupChildren) != 1 {
327+ t.Errorf("Expected parent to track 1 popup child, got %d", len(parentNode.activePopupChildren))
328+ }
329+
330+ if parentNode.activePopupChildren[0] != popupWebViewID {
331+ t.Errorf("Expected parent to track popup ID %s, got %s",
332+ popupWebViewID, parentNode.activePopupChildren[0])
333+ }
334+}
335+
336+// TestPopupCleanupGeneration tests cleanup generation tracking
337+func TestPopupCleanupGeneration(t *testing.T) {
338+ node := &paneNode{
339+ isLeaf: true,
340+ windowType: webkit.WindowTypePopup,
341+ isPopup: true,
342+ widgetValid: true,
343+ cleanupGeneration: 0,
344+ }
345+
346+ // Simulate lifecycle progression
347+ node.cleanupGeneration = 1
348+ if node.cleanupGeneration != 1 {
349+ t.Errorf("Expected cleanupGeneration 1, got %d", node.cleanupGeneration)
350+ }
351+
352+ // Simulate widget becoming invalid
353+ node.widgetValid = false
354+ if node.widgetValid {
355+ t.Errorf("Expected widgetValid to be false")
356+ }
357+
358+ // Increment generation for new lifecycle
359+ node.cleanupGeneration = 2
360+ if node.cleanupGeneration != 2 {
361+ t.Errorf("Expected cleanupGeneration 2, got %d", node.cleanupGeneration)
362+ }
363+}
364+
365+// TestMultiplePendingPopups tests handling multiple pending popups simultaneously
366+func TestMultiplePendingPopups(t *testing.T) {
367+ wm := &WorkspaceManager{
368+ pendingPopups: make(map[uint64]*pendingPopup),
369+ }
370+
371+ // Add multiple pending popups
372+ popups := map[uint64]string{
373+ 1: "https://popup1.com",
374+ 2: "https://popup2.com",
375+ 3: "https://popup3.com",
376+ }
377+
378+ for id, url := range popups {
379+ wm.pendingPopups[id] = &pendingPopup{
380+ url: url,
381+ }
382+ }
383+
384+ // Verify all stored
385+ if len(wm.pendingPopups) != 3 {
386+ t.Errorf("Expected 3 pending popups, got %d", len(wm.pendingPopups))
387+ }
388+
389+ // Verify each URL
390+ for id, expectedURL := range popups {
391+ pending, ok := wm.pendingPopups[id]
392+ if !ok {
393+ t.Errorf("Expected popup ID %d to be stored", id)
394+ continue
395+ }
396+ if pending.url != expectedURL {
397+ t.Errorf("Expected URL %s for popup %d, got %s", expectedURL, id, pending.url)
398+ }
399+ }
400+
401+ // Simulate ready-to-show for popup 2 (remove from pending)
402+ delete(wm.pendingPopups, 2)
403+
404+ // Verify remaining
405+ if len(wm.pendingPopups) != 2 {
406+ t.Errorf("Expected 2 pending popups after ready-to-show, got %d", len(wm.pendingPopups))
407+ }
408+
409+ // Verify popup 2 is gone but 1 and 3 remain
410+ if _, ok := wm.pendingPopups[2]; ok {
411+ t.Errorf("Expected popup 2 to be removed after ready-to-show")
412+ }
413+ if _, ok := wm.pendingPopups[1]; !ok {
414+ t.Errorf("Expected popup 1 to still be pending")
415+ }
416+ if _, ok := wm.pendingPopups[3]; !ok {
417+ t.Errorf("Expected popup 3 to still be pending")
418+ }
419+}
@@ -0,0 +1,419 @@
1+package browser
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/dumber/internal/config"
7+ "github.com/bnema/dumber/pkg/webkit"
8+)
9+
10+// TestPendingPopupStorage tests the pendingPopups map operations
11+func TestPendingPopupStorage(t *testing.T) {
12+ wm := &WorkspaceManager{
13+ pendingPopups: make(map[uint64]*pendingPopup),
14+ }
15+
16+ // Test adding a pending popup
17+ popupID := uint64(1)
18+ pending := &pendingPopup{
19+ url: "https://example.com",
20+ }
21+
22+ wm.pendingPopups[popupID] = pending
23+
24+ // Verify storage
25+ retrieved, ok := wm.pendingPopups[popupID]
26+ if !ok {
27+ t.Errorf("Expected popup ID %d to be stored", popupID)
28+ }
29+
30+ if retrieved.url != "https://example.com" {
31+ t.Errorf("Expected URL 'https://example.com', got %s", retrieved.url)
32+ }
33+
34+ // Test deletion
35+ delete(wm.pendingPopups, popupID)
36+ _, ok = wm.pendingPopups[popupID]
37+ if ok {
38+ t.Errorf("Expected popup ID %d to be deleted", popupID)
39+ }
40+}
41+
42+// TestPendingPopupCleanup tests cleanup of pending popups that never showed
43+func TestPendingPopupCleanup(t *testing.T) {
44+ wm := &WorkspaceManager{
45+ pendingPopups: make(map[uint64]*pendingPopup),
46+ }
47+
48+ // Add multiple pending popups
49+ for i := uint64(1); i <= 5; i++ {
50+ wm.pendingPopups[i] = &pendingPopup{
51+ url: "https://example.com",
52+ }
53+ }
54+
55+ // Verify all added
56+ if len(wm.pendingPopups) != 5 {
57+ t.Errorf("Expected 5 pending popups, got %d", len(wm.pendingPopups))
58+ }
59+
60+ // Simulate close before ready-to-show for popup 3
61+ popupID := uint64(3)
62+ if _, ok := wm.pendingPopups[popupID]; ok {
63+ delete(wm.pendingPopups, popupID)
64+ }
65+
66+ // Verify cleanup
67+ if len(wm.pendingPopups) != 4 {
68+ t.Errorf("Expected 4 pending popups after cleanup, got %d", len(wm.pendingPopups))
69+ }
70+
71+ _, ok := wm.pendingPopups[popupID]
72+ if ok {
73+ t.Errorf("Expected popup ID %d to be removed", popupID)
74+ }
75+}
76+
77+// TestPopupIDTracking tests that popup IDs are tracked correctly in paneNode
78+func TestPopupIDTracking(t *testing.T) {
79+ node := &paneNode{
80+ isLeaf: true,
81+ windowType: webkit.WindowTypePopup,
82+ isPopup: true,
83+ popupID: 42,
84+ }
85+
86+ // Verify popup ID is stored
87+ if node.popupID != 42 {
88+ t.Errorf("Expected popupID 42, got %d", node.popupID)
89+ }
90+
91+ // Verify window type
92+ if node.windowType != webkit.WindowTypePopup {
93+ t.Errorf("Expected WindowTypePopup, got %d", node.windowType)
94+ }
95+
96+ // Verify popup flag
97+ if !node.isPopup {
98+ t.Errorf("Expected isPopup to be true")
99+ }
100+}
101+
102+// TestActivePopupChildren tests tracking of active popup children in parent pane
103+func TestActivePopupChildren(t *testing.T) {
104+ parentNode := &paneNode{
105+ isLeaf: true,
106+ activePopupChildren: make([]string, 0),
107+ }
108+
109+ // Add popup children
110+ popupID1 := "1"
111+ popupID2 := "2"
112+ popupID3 := "3"
113+
114+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupID1)
115+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupID2)
116+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupID3)
117+
118+ // Verify count
119+ if len(parentNode.activePopupChildren) != 3 {
120+ t.Errorf("Expected 3 active popup children, got %d", len(parentNode.activePopupChildren))
121+ }
122+
123+ // Test removal of middle child
124+ for i, childID := range parentNode.activePopupChildren {
125+ if childID == popupID2 {
126+ parentNode.activePopupChildren = append(
127+ parentNode.activePopupChildren[:i],
128+ parentNode.activePopupChildren[i+1:]...,
129+ )
130+ break
131+ }
132+ }
133+
134+ // Verify removal
135+ if len(parentNode.activePopupChildren) != 2 {
136+ t.Errorf("Expected 2 active popup children after removal, got %d", len(parentNode.activePopupChildren))
137+ }
138+
139+ // Verify correct children remain
140+ if parentNode.activePopupChildren[0] != popupID1 || parentNode.activePopupChildren[1] != popupID3 {
141+ t.Errorf("Expected children [%s, %s], got %v", popupID1, popupID3, parentNode.activePopupChildren)
142+ }
143+}
144+
145+// TestShouldAutoClose tests OAuth URL detection logic
146+func TestShouldAutoClose(t *testing.T) {
147+ tests := []struct {
148+ name string
149+ url string
150+ enabled bool
151+ expected bool
152+ }{
153+ {
154+ name: "OAuth authorize URL",
155+ url: "https://accounts.google.com/o/oauth2/v2/auth?client_id=...",
156+ enabled: true,
157+ expected: true,
158+ },
159+ {
160+ name: "OAuth callback URL",
161+ url: "https://example.com/auth/callback?code=xyz",
162+ enabled: true,
163+ expected: true,
164+ },
165+ {
166+ name: "OAuth token response",
167+ url: "https://example.com/redirect?access_token=xyz",
168+ enabled: true,
169+ expected: true,
170+ },
171+ {
172+ name: "Regular URL - auto-close enabled",
173+ url: "https://example.com/page",
174+ enabled: true,
175+ expected: false,
176+ },
177+ {
178+ name: "OAuth URL - auto-close disabled",
179+ url: "https://accounts.google.com/o/oauth2/v2/auth?client_id=...",
180+ enabled: false,
181+ expected: false,
182+ },
183+ {
184+ name: "OpenID Connect URL",
185+ url: "https://example.com/oidc/authorize",
186+ enabled: true,
187+ expected: true,
188+ },
189+ {
190+ name: "Error response",
191+ url: "https://example.com/callback?error=access_denied",
192+ enabled: true,
193+ expected: true,
194+ },
195+ }
196+
197+ for _, tt := range tests {
198+ t.Run(tt.name, func(t *testing.T) {
199+ wm := &WorkspaceManager{
200+ app: &BrowserApp{
201+ config: &config.Config{
202+ Workspace: config.WorkspaceConfig{
203+ Popups: config.PopupBehaviorConfig{
204+ OAuthAutoClose: tt.enabled,
205+ },
206+ },
207+ },
208+ },
209+ }
210+
211+ result := wm.shouldAutoClose(tt.url)
212+ if result != tt.expected {
213+ t.Errorf("shouldAutoClose(%q) with enabled=%v = %v, want %v",
214+ tt.url, tt.enabled, result, tt.expected)
215+ }
216+ })
217+ }
218+}
219+
220+// TestPopupBehaviorConfig tests popup behavior configuration
221+func TestPopupBehaviorConfig(t *testing.T) {
222+ tests := []struct {
223+ name string
224+ behavior config.PopupBehavior
225+ }{
226+ {"Split behavior", config.PopupBehaviorSplit},
227+ {"Stacked behavior", config.PopupBehaviorStacked},
228+ {"Tabbed behavior", config.PopupBehaviorTabbed},
229+ {"Windowed behavior", config.PopupBehaviorWindowed},
230+ }
231+
232+ for _, tt := range tests {
233+ t.Run(tt.name, func(t *testing.T) {
234+ cfg := &config.Config{
235+ Workspace: config.WorkspaceConfig{
236+ Popups: config.PopupBehaviorConfig{
237+ Behavior: tt.behavior,
238+ },
239+ },
240+ }
241+
242+ // Verify behavior is stored correctly
243+ if cfg.Workspace.Popups.Behavior != tt.behavior {
244+ t.Errorf("Expected behavior %s, got %s",
245+ tt.behavior, cfg.Workspace.Popups.Behavior)
246+ }
247+ })
248+ }
249+}
250+
251+// TestPopupWindowTypeTracking tests window type tracking in paneNode
252+func TestPopupWindowTypeTracking(t *testing.T) {
253+ tests := []struct {
254+ name string
255+ windowType webkit.WindowType
256+ isPopup bool
257+ isRelated bool
258+ }{
259+ {
260+ name: "Regular tab",
261+ windowType: webkit.WindowTypeTab,
262+ isPopup: false,
263+ isRelated: false,
264+ },
265+ {
266+ name: "Popup window",
267+ windowType: webkit.WindowTypePopup,
268+ isPopup: true,
269+ isRelated: true,
270+ },
271+ }
272+
273+ for _, tt := range tests {
274+ t.Run(tt.name, func(t *testing.T) {
275+ node := &paneNode{
276+ windowType: tt.windowType,
277+ isPopup: tt.isPopup,
278+ isRelated: tt.isRelated,
279+ }
280+
281+ if node.windowType != tt.windowType {
282+ t.Errorf("Expected windowType %d, got %d", tt.windowType, node.windowType)
283+ }
284+
285+ if node.isPopup != tt.isPopup {
286+ t.Errorf("Expected isPopup %v, got %v", tt.isPopup, node.isPopup)
287+ }
288+
289+ if node.isRelated != tt.isRelated {
290+ t.Errorf("Expected isRelated %v, got %v", tt.isRelated, node.isRelated)
291+ }
292+ })
293+ }
294+}
295+
296+// TestPopupParentRelationship tests parent-child relationship tracking
297+func TestPopupParentRelationship(t *testing.T) {
298+ parentNode := &paneNode{
299+ isLeaf: true,
300+ activePopupChildren: make([]string, 0),
301+ }
302+
303+ popupNode := &paneNode{
304+ isLeaf: true,
305+ windowType: webkit.WindowTypePopup,
306+ isPopup: true,
307+ parentPane: parentNode,
308+ isRelated: true,
309+ }
310+
311+ // Verify parent relationship
312+ if popupNode.parentPane != parentNode {
313+ t.Errorf("Expected popup's parent to be parentNode")
314+ }
315+
316+ // Verify popup is marked as related
317+ if !popupNode.isRelated {
318+ t.Errorf("Expected popup to be marked as related")
319+ }
320+
321+ // Add popup to parent's children
322+ popupWebViewID := "test-popup-id"
323+ parentNode.activePopupChildren = append(parentNode.activePopupChildren, popupWebViewID)
324+
325+ // Verify parent tracks popup
326+ if len(parentNode.activePopupChildren) != 1 {
327+ t.Errorf("Expected parent to track 1 popup child, got %d", len(parentNode.activePopupChildren))
328+ }
329+
330+ if parentNode.activePopupChildren[0] != popupWebViewID {
331+ t.Errorf("Expected parent to track popup ID %s, got %s",
332+ popupWebViewID, parentNode.activePopupChildren[0])
333+ }
334+}
335+
336+// TestPopupCleanupGeneration tests cleanup generation tracking
337+func TestPopupCleanupGeneration(t *testing.T) {
338+ node := &paneNode{
339+ isLeaf: true,
340+ windowType: webkit.WindowTypePopup,
341+ isPopup: true,
342+ widgetValid: true,
343+ cleanupGeneration: 0,
344+ }
345+
346+ // Simulate lifecycle progression
347+ node.cleanupGeneration = 1
348+ if node.cleanupGeneration != 1 {
349+ t.Errorf("Expected cleanupGeneration 1, got %d", node.cleanupGeneration)
350+ }
351+
352+ // Simulate widget becoming invalid
353+ node.widgetValid = false
354+ if node.widgetValid {
355+ t.Errorf("Expected widgetValid to be false")
356+ }
357+
358+ // Increment generation for new lifecycle
359+ node.cleanupGeneration = 2
360+ if node.cleanupGeneration != 2 {
361+ t.Errorf("Expected cleanupGeneration 2, got %d", node.cleanupGeneration)
362+ }
363+}
364+
365+// TestMultiplePendingPopups tests handling multiple pending popups simultaneously
366+func TestMultiplePendingPopups(t *testing.T) {
367+ wm := &WorkspaceManager{
368+ pendingPopups: make(map[uint64]*pendingPopup),
369+ }
370+
371+ // Add multiple pending popups
372+ popups := map[uint64]string{
373+ 1: "https://popup1.com",
374+ 2: "https://popup2.com",
375+ 3: "https://popup3.com",
376+ }
377+
378+ for id, url := range popups {
379+ wm.pendingPopups[id] = &pendingPopup{
380+ url: url,
381+ }
382+ }
383+
384+ // Verify all stored
385+ if len(wm.pendingPopups) != 3 {
386+ t.Errorf("Expected 3 pending popups, got %d", len(wm.pendingPopups))
387+ }
388+
389+ // Verify each URL
390+ for id, expectedURL := range popups {
391+ pending, ok := wm.pendingPopups[id]
392+ if !ok {
393+ t.Errorf("Expected popup ID %d to be stored", id)
394+ continue
395+ }
396+ if pending.url != expectedURL {
397+ t.Errorf("Expected URL %s for popup %d, got %s", expectedURL, id, pending.url)
398+ }
399+ }
400+
401+ // Simulate ready-to-show for popup 2 (remove from pending)
402+ delete(wm.pendingPopups, 2)
403+
404+ // Verify remaining
405+ if len(wm.pendingPopups) != 2 {
406+ t.Errorf("Expected 2 pending popups after ready-to-show, got %d", len(wm.pendingPopups))
407+ }
408+
409+ // Verify popup 2 is gone but 1 and 3 remain
410+ if _, ok := wm.pendingPopups[2]; ok {
411+ t.Errorf("Expected popup 2 to be removed after ready-to-show")
412+ }
413+ if _, ok := wm.pendingPopups[1]; !ok {
414+ t.Errorf("Expected popup 1 to still be pending")
415+ }
416+ if _, ok := wm.pendingPopups[3]; !ok {
417+ t.Errorf("Expected popup 3 to still be pending")
418+ }
419+}

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