v0.33.0 62a09f5

webview_popup_routing_test.go RawHistoryFolder

1package cef
2
3import (
4	"bytes"
5	"context"
6	"encoding/json"
7	"sync/atomic"
8	"testing"
9	"time"
10
11	purecef "github.com/bnema/purego-cef/cef"
12	"github.com/rs/zerolog"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/require"
15
16	"github.com/bnema/dumber/internal/application/dto"
17	"github.com/bnema/dumber/internal/application/port"
18	portmocks "github.com/bnema/dumber/internal/application/port/mocks"
19	"github.com/bnema/dumber/internal/logging"
20)
21
22func TestMapCEFPopupFeatures(t *testing.T) {
23	t.Parallel()
24
25	tests := []struct {
26		name string
27		in   *purecef.PopupFeatures
28		want dto.PopupFeatures
29	}{
30		{
31			name: "nil is known featureless",
32			want: dto.PopupFeatures{State: dto.PopupFeaturesNone},
33		},
34		{
35			name: "positive width is popup-like",
36			in:   &purecef.PopupFeatures{Width: 640, Widthset: 1},
37			want: dto.PopupFeatures{
38				State: dto.PopupFeaturesSpecified, Width: 640, WidthSet: true, IsPopupSet: true,
39			},
40		},
41		{
42			name: "geometry and flags are normalized",
43			in: &purecef.PopupFeatures{
44				X: 10, Xset: 1, Y: 20, Yset: 1,
45				Width: 800, Widthset: 1, Height: 600, Heightset: 1,
46			},
47			want: dto.PopupFeatures{
48				State: dto.PopupFeaturesSpecified,
49				X:     10, XSet: true, Y: 20, YSet: true,
50				Width: 800, WidthSet: true, Height: 600, HeightSet: true,
51				IsPopupSet: true,
52			},
53		},
54		{
55			name: "is popup flag survives without inventing geometry",
56			in:   &purecef.PopupFeatures{Ispopup: 1},
57			want: dto.PopupFeatures{
58				State: dto.PopupFeaturesSpecified, IsPopup: true, IsPopupSet: true,
59			},
60		},
61		{
62			name: "allocated defaults are known featureless",
63			in:   &purecef.PopupFeatures{},
64			want: dto.PopupFeatures{State: dto.PopupFeaturesNone, IsPopupSet: true},
65		},
66	}
67
68	for _, tt := range tests {
69		t.Run(tt.name, func(t *testing.T) {
70			got := mapCEFPopupFeatures(tt.in)
71			assert.Equal(t, tt.want, got)
72		})
73	}
74}
75
76func TestOnBeforePopup_PropagatesCEFEngineFeaturesGestureAndNoJavaScriptAccess(t *testing.T) {
77	parentWV := &WebView{ctx: context.Background(), id: 16}
78	popupWV := &WebView{ctx: context.Background(), id: 24, pendingCreate: &pendingBrowserCreate{}}
79	popupWV.markNativePopupCandidate(parentWV)
80	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateBrowserWindow})
81	noJavaScriptAccess := true
82	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(req port.PopupRequest) port.WebView {
83		assert.Equal(t, dto.BrowserEngineCEF, req.Engine)
84		assert.Equal(t, dto.PopupFeatures{
85			State: dto.PopupFeaturesSpecified, X: 10, XSet: true, Width: 640, WidthSet: true,
86			IsPopup: true, IsPopupSet: true,
87		}, req.PopupFeatures)
88		assert.Equal(t, dto.WindowDispositionNewPopup, req.TargetDisposition)
89		assert.True(t, req.IsUserGesture)
90		assert.True(t, req.NoJavaScriptAccess)
91		return popupWV
92	}})
93
94	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
95		nil, nil, 90, "https://example.com/popup", "named-popup",
96		purecef.WindowOpenDispositionWodNewPopup, 1,
97		&purecef.PopupFeatures{X: 10, Xset: 1, Width: 640, Widthset: 1, Ispopup: 1},
98		nil, nil, nil, nil, &noJavaScriptAccess,
99	)
100
101	require.True(t, blocked)
102	require.True(t, popupWV.nativePopupFallbackStarted)
103	require.Nil(t, popupWV.popupOpenerBridgeParent, "noopener must not install the synthetic opener bridge")
104}
105
106func TestOnBeforePopup_FeaturelessBlankNavigateSourceBlocksNativePopup(t *testing.T) {
107	const targetURI = "https://example.com/source-navigation"
108	parentWV := &WebView{ctx: context.Background(), id: 17}
109	candidate := portmocks.NewMockWebView(t)
110	candidate.EXPECT().Destroy().Once()
111
112	var callbackResult port.WebView = candidate
113	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(req port.PopupRequest) port.WebView {
114		require.Equal(t, dto.BrowserEngineCEF, req.Engine)
115		require.Equal(t, "_blank", req.FrameName)
116		require.Equal(t, dto.WindowDispositionNewTab, req.TargetDisposition)
117		require.Equal(t, dto.PopupFeaturesNone, req.PopupFeatures.State)
118
119		// NavigateSource ownership belongs to the coordinator callback: navigate
120		// the source, destroy its unused candidate exactly once, then return nil.
121		require.NoError(t, parentWV.LoadURI(context.Background(), req.TargetURI))
122		candidate.Destroy()
123		callbackResult = nil
124		return callbackResult
125	}})
126
127	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
128		nil, nil, 91, targetURI, "_blank",
129		purecef.WindowOpenDispositionWodNewForegroundTab, 1, nil, nil, nil, nil, nil, nil,
130	)
131
132	require.True(t, blocked, "CEF native popup creation must remain blocked")
133	require.Nil(t, callbackResult, "NavigateSource callback must return nil")
134	require.Equal(t, targetURI, parentWV.pendingNavigationURI())
135}
136
137func TestOnBeforePopup_BrowserWindowDecisionPreservesSyntheticOpenerBridge(t *testing.T) {
138	parentWV := &WebView{ctx: context.Background(), id: 17}
139	parentWV.updateURI("https://example.com/opener")
140	popupWV := &WebView{ctx: context.Background(), id: 25, pendingCreate: &pendingBrowserCreate{}}
141	popupWV.markNativePopupCandidate(parentWV)
142	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateBrowserWindow})
143	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
144
145	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
146		nil, nil, 91, "https://example.com/child", "child",
147		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
148	)
149
150	require.True(t, blocked)
151	require.True(t, popupWV.nativePopupFallbackStarted)
152	require.Same(t, parentWV, popupWV.popupOpenerBridgeParent)
153	require.Equal(t, "https://example.com/opener", popupWV.popupOpenerBridgeParentURI)
154}
155
156func TestOnBeforePopup_NativeArmFailurePreparesEligibleFallbackBeforeAbort(t *testing.T) {
157	parentWV := &WebView{ctx: context.Background(), id: 18}
158	parentWV.updateURI("https://example.com/opener")
159	popupWV := &WebView{ctx: context.Background(), id: 26, pendingCreate: &pendingBrowserCreate{}}
160	popupWV.markNativePopupCandidate(parentWV)
161	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})
162	abortCalls := 0
163	popupWV.SetNativePopupHostAbort(func() {
164		abortCalls++
165		assert.True(t, popupWV.nativePopupFallbackStarted)
166		assert.Same(t, parentWV, popupWV.popupOpenerBridgeParent)
167	})
168	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
169
170	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
171		nil, nil, 92, "https://example.com/visual", "visual",
172		purecef.WindowOpenDispositionWodNewPopup, 1,
173		&purecef.PopupFeatures{Width: 640, Widthset: 1}, nil, nil, nil, nil, nil,
174	)
175
176	require.True(t, blocked)
177	require.Equal(t, 1, abortCalls)
178	require.False(t, popupWV.IsDestroyed())
179}
180
181func TestOnBeforePopup_OpenerRequiredNativeArmFailureDeniesWithoutFallback(t *testing.T) {
182	var output bytes.Buffer
183	logger := zerolog.New(&output).Level(zerolog.WarnLevel)
184	ctx := logging.WithContext(context.Background(), logger)
185	parentWV := &WebView{ctx: ctx, id: 19}
186	popupWV := &WebView{ctx: context.Background(), id: 27, pendingCreate: &pendingBrowserCreate{}}
187	popupWV.markNativePopupCandidate(parentWV)
188	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{
189		Kind: dto.HostDecisionCreateNativePopup, SourceHost: dto.SourceHostFloating, RequiresNativeOpener: true,
190	})
191	abortCalls := 0
192	popupWV.SetNativePopupHostAbort(func() {
193		abortCalls++
194		popupWV.Destroy()
195	})
196	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
197
198	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
199		nil, nil, 93, "https://example.com/opener-required", "required",
200		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
201	)
202
203	require.True(t, blocked)
204	require.Equal(t, 1, abortCalls)
205	require.False(t, popupWV.nativePopupFallbackStarted)
206	require.Nil(t, popupWV.popupOpenerBridgeParent)
207	require.True(t, popupWV.IsDestroyed())
208	var record map[string]any
209	require.NoError(t, json.Unmarshal(output.Bytes(), &record))
210	assert.Equal(t, "cef", record["engine"])
211	assert.Equal(t, "floating", record["source_host"])
212	assert.Equal(t, "create-native-popup", record["decision"])
213	assert.Equal(t, "new-popup", record["target_disposition"])
214	assert.Equal(t, "native-arm-failed", record["reason_code"])
215	assert.Equal(t, true, record["host_abort_invoked"])
216	assert.Equal(t, "cef: native popup arming failed", record["message"])
217}
218
219func TestOnBeforePopup_NativeArmFailureWithoutHostAbortLogsDirectCleanup(t *testing.T) {
220	var output bytes.Buffer
221	logger := zerolog.New(&output).Level(zerolog.WarnLevel)
222	ctx := logging.WithContext(context.Background(), logger)
223	parentWV := &WebView{ctx: ctx, id: 20}
224	popupWV := &WebView{ctx: context.Background(), id: 28, pendingCreate: &pendingBrowserCreate{}}
225	popupWV.markNativePopupCandidate(parentWV)
226	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{
227		Kind: dto.HostDecisionCreateNativePopup, RequiresNativeOpener: true,
228	})
229	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
230
231	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
232		nil, nil, 94, "https://example.com/opener-required", "required",
233		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
234	)
235
236	require.True(t, blocked)
237	require.True(t, popupWV.IsDestroyed())
238	var record map[string]any
239	require.NoError(t, json.Unmarshal(output.Bytes(), &record))
240	assert.Equal(t, false, record["host_abort_invoked"])
241	assert.Equal(t, "cef: native popup arming failed", record["message"])
242}
243
244func TestOnBeforePopup_AuthNativeArmFailureDeniesWithoutFallback(t *testing.T) {
245	parentWV := &WebView{ctx: context.Background(), id: 20}
246	popupWV := &WebView{ctx: context.Background(), id: 28, pendingCreate: &pendingBrowserCreate{}}
247	popupWV.markNativePopupCandidate(parentWV)
248	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{
249		Kind: dto.HostDecisionCreateNativePopup, ReasonCode: dto.HostDecisionReasonAuthNativePopup,
250	})
251	popupWV.SetNativePopupHostAbort(func() { popupWV.Destroy() })
252	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
253
254	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
255		nil, nil, 94, "https://accounts.example.test/oauth", "oauth",
256		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
257	)
258
259	require.True(t, blocked)
260	require.False(t, popupWV.nativePopupFallbackStarted)
261	require.True(t, popupWV.IsDestroyed())
262}
263
264func TestOnBeforePopup_IsPopupFlagCanSelectNativePopup(t *testing.T) {
265	parentWV := &WebView{ctx: context.Background(), id: 22}
266	popupWV := &WebView{ctx: context.Background(), id: 30, pendingCreate: &pendingBrowserCreate{}}
267	popupWV.markNativePopupCandidate(parentWV)
268	abortCalls := 0
269	popupWV.SetNativePopupHostAbort(func() { abortCalls++ })
270	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(req port.PopupRequest) port.WebView {
271		require.Equal(t, dto.PopupFeaturesSpecified, req.PopupFeatures.State)
272		require.True(t, req.PopupFeatures.IsPopup)
273		require.True(t, req.PopupFeatures.IsPopupSet)
274		popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})
275		return popupWV
276	}})
277
278	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
279		nil, nil, 96, "https://example.com/chrome-restricted", "restricted",
280		purecef.WindowOpenDispositionWodNewPopup, 1,
281		&purecef.PopupFeatures{Ispopup: 1}, nil, nil, nil, nil, nil,
282	)
283
284	require.True(t, blocked)
285	require.Equal(t, 1, abortCalls)
286}
287
288func TestOnBeforePopup_ReuseNamedPaneBlocksNativePopup(t *testing.T) {
289	parentWV := &WebView{ctx: context.Background(), id: 23}
290	popupWV := &WebView{ctx: context.Background(), id: 31, pendingCreate: &pendingBrowserCreate{}}
291	popupWV.markNativePopupCandidate(parentWV)
292	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionReuseNamedPane})
293	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
294
295	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
296		nil, nil, 97, "https://example.com/named", "shared",
297		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
298	)
299
300	require.True(t, blocked)
301	require.True(t, popupWV.nativePopupFallbackStarted)
302}
303
304func TestOnBeforePopup_HostedDecisionDiscardsUnpreparedNativeCandidate(t *testing.T) {
305	parentWV := &WebView{ctx: context.Background(), id: 24}
306	popupWV := &WebView{ctx: context.Background(), id: 32}
307	popupWV.markNativePopupCandidate(parentWV)
308	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateBrowserWindow})
309	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
310
311	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
312		nil, nil, 98, "https://example.com/window", "window",
313		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
314	)
315
316	require.True(t, blocked)
317	require.False(t, popupWV.isNativePopupCandidate())
318	require.False(t, popupWV.nativePopupFallbackStarted)
319}
320
321func TestOnBeforePopup_HostedDecisionPreservesAlreadyPreparedFallbackOpener(t *testing.T) {
322	parentWV := &WebView{ctx: context.Background(), id: 25}
323	parentWV.updateURI("https://example.com/opener")
324	popupWV := &WebView{ctx: context.Background(), id: 33, pendingCreate: &pendingBrowserCreate{}}
325	popupWV.markNativePopupCandidate(parentWV)
326	require.True(t, popupWV.preparePopupShellDirectBrowserCreation())
327	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateBrowserWindow})
328	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
329
330	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
331		nil, nil, 99, "https://example.com/window", "window",
332		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
333	)
334
335	require.True(t, blocked)
336	require.True(t, popupWV.nativePopupFallbackStarted)
337	require.Same(t, parentWV, popupWV.popupOpenerBridgeParent)
338	require.Equal(t, "https://example.com/opener", popupWV.popupOpenerBridgeParentURI)
339}
340
341func TestOnBeforePopup_DenyCleansUp(t *testing.T) {
342	parentWV := &WebView{ctx: context.Background(), id: 24}
343	popupWV := &WebView{ctx: context.Background(), id: 32}
344	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionDeny})
345	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
346
347	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
348		nil, nil, 98, "https://example.com/denied", "denied",
349		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
350	)
351
352	require.True(t, blocked)
353	require.True(t, popupWV.IsDestroyed())
354}
355
356func TestOnBeforePopup_AwaitFeaturesIsImpossibleAndCleansUp(t *testing.T) {
357	parentWV := &WebView{ctx: context.Background(), id: 21}
358	popupWV := &WebView{ctx: context.Background(), id: 29}
359	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionAwaitPopupFeatures})
360	parentWV.SetCallbacks(&port.WebViewCallbacks{OnCreate: func(port.PopupRequest) port.WebView { return popupWV }})
361
362	blocked := (&handlerSet{wv: parentWV}).OnBeforePopup(
363		nil, nil, 95, "https://example.com/impossible", "impossible",
364		purecef.WindowOpenDispositionWodNewPopup, 1, nil, nil, nil, nil, nil, nil,
365	)
366
367	require.True(t, blocked)
368	require.True(t, popupWV.IsDestroyed())
369}
370
371func TestOnBeforePopup_TimesOutGTKDispatchAndBlocksPopup(t *testing.T) {
372	delayed := make(chan func(), 1)
373	parentWV := &WebView{
374		ctx:            context.Background(),
375		id:             16,
376		gtkSyncTimeout: 5 * time.Millisecond,
377		gtkSyncIsOwner: func() bool { return false },
378		gtkSyncDispatch: func(fn func()) {
379			delayed <- fn
380		},
381	}
382	var createCalls atomic.Int32
383	parentWV.SetCallbacks(&port.WebViewCallbacks{
384		OnCreate: func(_ port.PopupRequest) port.WebView {
385			createCalls.Add(1)
386			return &WebView{ctx: context.Background(), id: 24}
387		},
388	})
389
390	h := &handlerSet{wv: parentWV}
391	blocked := h.OnBeforePopup(nil, nil, 90, "https://example.com/slow-popup", "slow-popup", 0, 1, nil, nil, nil, nil, nil, nil)
392
393	require.True(t, blocked)
394	require.Zero(t, createCalls.Load())
395
396	select {
397	case fn := <-delayed:
398		fn()
399	case <-time.After(time.Second):
400		t.Fatal("timed out waiting for delayed popup dispatch")
401	}
402	require.Zero(t, createCalls.Load())
403}
404
405func TestOnBeforePopup_PrimesPopupNavigationWhenCEFPopupBlocksNativeCreation(t *testing.T) {
406	parentWV := &WebView{ctx: context.Background(), id: 17}
407	popupWV := &WebView{ctx: context.Background(), id: 23}
408	parentWV.SetCallbacks(&port.WebViewCallbacks{
409		OnCreate: func(req port.PopupRequest) port.WebView {
410			require.Equal(t, "https://example.com/popup", req.TargetURI)
411			require.Equal(t, "auth-popup", req.FrameName)
412			require.True(t, req.IsUserGesture)
413			popupWV.PrimePopupNavigation(req.TargetURI)
414			return popupWV
415		},
416	})
417
418	h := &handlerSet{wv: parentWV}
419	blocked := h.OnBeforePopup(nil, nil, 91, "https://example.com/popup", "auth-popup", 0, 1, nil, nil, nil, nil, nil, nil)
420
421	require.True(t, blocked)
422	require.Equal(t, "https://example.com/popup", popupWV.pendingNavigationURI())
423}
424
425func TestOnBeforePopup_PrimesPopupShellWhenNativePopupCannotBeArmed(t *testing.T) {
426	parentWV := &WebView{ctx: context.Background(), id: 31}
427	popupWV := &WebView{ctx: context.Background(), id: 32}
428	popupWV.markNativePopupCandidate(parentWV)
429	parentWV.SetCallbacks(&port.WebViewCallbacks{
430		OnCreate: func(req port.PopupRequest) port.WebView {
431			require.Equal(t, "https://example.com/login", req.TargetURI)
432			popupWV.PrimePopupNavigation(req.TargetURI)
433			return popupWV
434		},
435	})
436
437	h := &handlerSet{wv: parentWV}
438	blocked := h.OnBeforePopup(nil, nil, 77, "https://example.com/login", "Google login", 0, 1, nil, nil, nil, nil, nil, nil)
439
440	require.True(t, blocked)
441	require.False(t, popupWV.isNativePopupCandidate())
442	require.Equal(t, "https://example.com/login", popupWV.pendingNavigationURI())
443}
444
445func TestOnBeforePopup_PaneDecisionBlocksNativePopupAndKeepsPanePath(t *testing.T) {
446	parentWV := &WebView{ctx: context.Background(), id: 41}
447	popupWV := &WebView{ctx: context.Background(), id: 42}
448	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreatePane})
449	parentWV.SetCallbacks(&port.WebViewCallbacks{
450		OnCreate: func(req port.PopupRequest) port.WebView {
451			popupWV.PreparePaneHostedBrowsingContext()
452			popupWV.PrimePopupNavigation(req.TargetURI)
453			return popupWV
454		},
455	})
456
457	h := &handlerSet{wv: parentWV}
458	blocked := h.OnBeforePopup(nil, nil, 81, "https://example.com/pane", "_blank", 0, 1, nil, nil, nil, nil, nil, nil)
459
460	require.True(t, blocked)
461	require.Equal(t, "https://example.com/pane", popupWV.pendingNavigationURI())
462}
463
464func TestOnBeforePopup_NativeDecisionAbortsHostWhenArmingFails(t *testing.T) {
465	parentWV := &WebView{ctx: context.Background(), id: 51}
466	popupWV := &WebView{ctx: context.Background(), id: 52}
467	popupWV.markNativePopupCandidate(parentWV)
468	popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})
469	aborted := false
470	popupWV.SetNativePopupHostAbort(func() { aborted = true })
471	parentWV.SetCallbacks(&port.WebViewCallbacks{
472		OnCreate: func(req port.PopupRequest) port.WebView {
473			popupWV.PrimePopupNavigation(req.TargetURI)
474			return popupWV
475		},
476	})
477
478	h := &handlerSet{wv: parentWV}
479	blocked := h.OnBeforePopup(nil, nil, 82, "https://accounts.google.com/o/oauth2/v2/auth", "oauth", 0, 1, nil, nil, nil, nil, nil, nil)
480
481	require.True(t, blocked)
482	require.True(t, aborted)
483}

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