detached c5be254

fix(ui): preserve opener-capable popup fallback

commit
c5be2543c82bb43ae02aa2cccdce76db961f4d83
author
brice · · 2026-07-25 07:44 UTC
committer
brice · · 2026-07-26 16:42 UTC
parent
8b31f32
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +121 -61
M internal/ui/coordinator/content/popup_hosting.go +66 -0
@@ -3,6 +3,7 @@ package content
33 import (
44 "context"
55 "fmt"
6+ "sync"
67
78 "github.com/bnema/dumber/internal/application/dto"
89 "github.com/bnema/dumber/internal/application/port"
@@ -59,6 +60,71 @@ func (pm *popupManager) openBrowserWindow(
5960 return popupWV
6061 }
6162
63+func popupSupportsOpenerBridge(wv port.WebView) bool {
64+ _, ok := wv.(port.PopupOpenerCapable)
65+ return ok
66+}
67+
68+func (pm *popupManager) openNativePopup(
69+ ctx context.Context,
70+ hooks popupCoordinatorHooks,
71+ parentPaneID entity.PaneID,
72+ parentID port.WebViewID,
73+ parentURIAtOpen string,
74+ req port.PopupRequest,
75+ decision dto.HostDecision,
76+) port.WebView {
77+ log := logging.FromContext(ctx)
78+ normalized := buildPopupBrowsingContextRequest(req)
79+ normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
80+ if pm.onOpenNativePopup == nil {
81+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
82+ return nil
83+ }
84+ popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
85+ if err != nil {
86+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
87+ return nil
88+ }
89+ cfg := pm.currentPopupConfig()
90+ pm.setBrowsingContextDecision(popupWV, decision)
91+ allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener &&
92+ (req.NoJavaScriptAccess || popupSupportsOpenerBridge(popupWV))
93+ var abortOnce sync.Once
94+ abortResult := false
95+ onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
96+ abortOnce.Do(func() {
97+ if !allowFallback {
98+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
99+ return
100+ }
101+ abortResult = pm.openExistingPopupInBrowserWindow(
102+ abortCtx, hooks, parentPaneID, parentID, abortedWV, req, decision, true,
103+ )
104+ })
105+ return abortResult
106+ }
107+ if err := pm.onOpenNativePopup(ctx, NativePopupInput{
108+ ParentPaneID: parentPaneID,
109+ ParentWebViewID: parentID,
110+ ParentURIAtOpen: parentURIAtOpen,
111+ PopupWebView: popupWV,
112+ TargetURI: req.TargetURI,
113+ Request: req,
114+ ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
115+ AllowBrowserWindowFallback: allowFallback,
116+ OnNativeHostAbort: onAbort,
117+ }); err != nil {
118+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, err)
119+ popupWV.Destroy()
120+ return nil
121+ }
122+ if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
123+ lifecycle.PrimePopupNavigation(req.TargetURI)
124+ }
125+ return popupWV
126+}
127+
62128 // openExistingPopupInBrowserWindow attempts to transfer an already-created
63129 // WebView. On failure ownership remains with the caller.
64130 func (pm *popupManager) openExistingPopupInBrowserWindow(
@@ -3,6 +3,7 @@ package content
3 import (3 import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6+ "sync"
6 7
7 "github.com/bnema/dumber/internal/application/dto"8 "github.com/bnema/dumber/internal/application/dto"
8 "github.com/bnema/dumber/internal/application/port"9 "github.com/bnema/dumber/internal/application/port"
@@ -59,6 +60,71 @@ func (pm *popupManager) openBrowserWindow(
59 return popupWV60 return popupWV
60 }61 }
61 62
63+func popupSupportsOpenerBridge(wv port.WebView) bool {
64+ _, ok := wv.(port.PopupOpenerCapable)
65+ return ok
66+}
67+
68+func (pm *popupManager) openNativePopup(
69+ ctx context.Context,
70+ hooks popupCoordinatorHooks,
71+ parentPaneID entity.PaneID,
72+ parentID port.WebViewID,
73+ parentURIAtOpen string,
74+ req port.PopupRequest,
75+ decision dto.HostDecision,
76+) port.WebView {
77+ log := logging.FromContext(ctx)
78+ normalized := buildPopupBrowsingContextRequest(req)
79+ normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
80+ if pm.onOpenNativePopup == nil {
81+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
82+ return nil
83+ }
84+ popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
85+ if err != nil {
86+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
87+ return nil
88+ }
89+ cfg := pm.currentPopupConfig()
90+ pm.setBrowsingContextDecision(popupWV, decision)
91+ allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener &&
92+ (req.NoJavaScriptAccess || popupSupportsOpenerBridge(popupWV))
93+ var abortOnce sync.Once
94+ abortResult := false
95+ onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
96+ abortOnce.Do(func() {
97+ if !allowFallback {
98+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
99+ return
100+ }
101+ abortResult = pm.openExistingPopupInBrowserWindow(
102+ abortCtx, hooks, parentPaneID, parentID, abortedWV, req, decision, true,
103+ )
104+ })
105+ return abortResult
106+ }
107+ if err := pm.onOpenNativePopup(ctx, NativePopupInput{
108+ ParentPaneID: parentPaneID,
109+ ParentWebViewID: parentID,
110+ ParentURIAtOpen: parentURIAtOpen,
111+ PopupWebView: popupWV,
112+ TargetURI: req.TargetURI,
113+ Request: req,
114+ ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
115+ AllowBrowserWindowFallback: allowFallback,
116+ OnNativeHostAbort: onAbort,
117+ }); err != nil {
118+ logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, err)
119+ popupWV.Destroy()
120+ return nil
121+ }
122+ if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
123+ lifecycle.PrimePopupNavigation(req.TargetURI)
124+ }
125+ return popupWV
126+}
127+
62 // openExistingPopupInBrowserWindow attempts to transfer an already-created128 // openExistingPopupInBrowserWindow attempts to transfer an already-created
63 // WebView. On failure ownership remains with the caller.129 // WebView. On failure ownership remains with the caller.
64 func (pm *popupManager) openExistingPopupInBrowserWindow(130 func (pm *popupManager) openExistingPopupInBrowserWindow(
M internal/ui/coordinator/content/popup_manager.go +0 -59
@@ -512,65 +512,6 @@ func (pm *popupManager) popupParentURIAtOpen(
512512 return parentURIAtOpen
513513 }
514514
515-func (pm *popupManager) openNativePopup(
516- ctx context.Context,
517- hooks popupCoordinatorHooks,
518- parentPaneID entity.PaneID,
519- parentID port.WebViewID,
520- parentURIAtOpen string,
521- req port.PopupRequest,
522- decision dto.HostDecision,
523-) port.WebView {
524- log := logging.FromContext(ctx)
525- normalized := buildPopupBrowsingContextRequest(req)
526- normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
527- if pm.onOpenNativePopup == nil {
528- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
529- return nil
530- }
531- popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
532- if err != nil {
533- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
534- return nil
535- }
536- cfg := pm.currentPopupConfig()
537- pm.setBrowsingContextDecision(popupWV, decision)
538- allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener && req.NoJavaScriptAccess
539- var abortOnce sync.Once
540- abortResult := false
541- onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
542- abortOnce.Do(func() {
543- if !allowFallback {
544- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
545- return
546- }
547- abortResult = pm.openExistingPopupInBrowserWindow(
548- abortCtx, hooks, parentPaneID, parentID, abortedWV, req, decision, true,
549- )
550- })
551- return abortResult
552- }
553- if err := pm.onOpenNativePopup(ctx, NativePopupInput{
554- ParentPaneID: parentPaneID,
555- ParentWebViewID: parentID,
556- ParentURIAtOpen: parentURIAtOpen,
557- PopupWebView: popupWV,
558- TargetURI: req.TargetURI,
559- Request: req,
560- ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
561- AllowBrowserWindowFallback: allowFallback,
562- OnNativeHostAbort: onAbort,
563- }); err != nil {
564- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, err)
565- popupWV.Destroy()
566- return nil
567- }
568- if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
569- lifecycle.PrimePopupNavigation(req.TargetURI)
570- }
571- return popupWV
572-}
573-
574515 func (pm *popupManager) handlePopupCreate(
575516 ctx context.Context,
576517 hooks popupCoordinatorHooks,
@@ -512,65 +512,6 @@ func (pm *popupManager) popupParentURIAtOpen(
512 return parentURIAtOpen512 return parentURIAtOpen
513 }513 }
514 514
515-func (pm *popupManager) openNativePopup(
516- ctx context.Context,
517- hooks popupCoordinatorHooks,
518- parentPaneID entity.PaneID,
519- parentID port.WebViewID,
520- parentURIAtOpen string,
521- req port.PopupRequest,
522- decision dto.HostDecision,
523-) port.WebView {
524- log := logging.FromContext(ctx)
525- normalized := buildPopupBrowsingContextRequest(req)
526- normalized.SourceHost = pm.resolveSourceHost(parentPaneID)
527- if pm.onOpenNativePopup == nil {
528- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostUnavailable, nil)
529- return nil
530- }
531- popupWV, err := pm.createPopupWebView(ctx, parentID, req.TargetURI, false)
532- if err != nil {
533- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureHostFailed, err)
534- return nil
535- }
536- cfg := pm.currentPopupConfig()
537- pm.setBrowsingContextDecision(popupWV, decision)
538- allowFallback := !normalized.AuthIntent && !decision.RequiresNativeOpener && req.NoJavaScriptAccess
539- var abortOnce sync.Once
540- abortResult := false
541- onAbort := func(abortCtx context.Context, abortedWV port.WebView) bool {
542- abortOnce.Do(func() {
543- if !allowFallback {
544- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, nil)
545- return
546- }
547- abortResult = pm.openExistingPopupInBrowserWindow(
548- abortCtx, hooks, parentPaneID, parentID, abortedWV, req, decision, true,
549- )
550- })
551- return abortResult
552- }
553- if err := pm.onOpenNativePopup(ctx, NativePopupInput{
554- ParentPaneID: parentPaneID,
555- ParentWebViewID: parentID,
556- ParentURIAtOpen: parentURIAtOpen,
557- PopupWebView: popupWV,
558- TargetURI: req.TargetURI,
559- Request: req,
560- ObserveOAuthAutoClose: cfg != nil && cfg.OAuthAutoClose && IsOAuthURL(req.TargetURI),
561- AllowBrowserWindowFallback: allowFallback,
562- OnNativeHostAbort: onAbort,
563- }); err != nil {
564- logBrowsingContextFailure(*log, normalized, decision, dto.BrowsingContextFailureNativeArm, err)
565- popupWV.Destroy()
566- return nil
567- }
568- if lifecycle, ok := popupWV.(port.PopupLifecycleCapable); ok {
569- lifecycle.PrimePopupNavigation(req.TargetURI)
570- }
571- return popupWV
572-}
573-
574 func (pm *popupManager) handlePopupCreate(515 func (pm *popupManager) handlePopupCreate(
575 ctx context.Context,516 ctx context.Context,
576 hooks popupCoordinatorHooks,517 hooks popupCoordinatorHooks,
M internal/ui/coordinator/content/popup_test.go +55 -2
@@ -1,11 +1,14 @@
11 package content
22
33 import (
4+ "bytes"
45 "context"
56 "errors"
67 "sync"
78 "testing"
89
10+ "github.com/rs/zerolog"
11+
912 "github.com/stretchr/testify/assert"
1013 "github.com/stretchr/testify/mock"
1114 "github.com/stretchr/testify/require"
@@ -15,6 +18,7 @@ import (
1518 "github.com/bnema/dumber/internal/application/port/mocks"
1619 "github.com/bnema/dumber/internal/domain/entity"
1720 domainerrors "github.com/bnema/dumber/internal/domain/errors"
21+ "github.com/bnema/dumber/internal/logging"
1822 )
1923
2024 // ---------------------------------------------------------------------------
@@ -674,6 +678,35 @@ func TestPopupNativeAbortFallsBackOnceForNoOpenerVisualPopup(t *testing.T) {
674678 assert.Equal(t, 1, browserCalls)
675679 }
676680
681+func TestPopupNativeAbortFallsBackForOpenerCapableVisualPopup(t *testing.T) {
682+ ctx := context.Background()
683+ popupWV := &popupOpenerWebViewStub{popupNavigationWebViewStub: &popupNavigationWebViewStub{MockWebView: mocks.NewMockWebView(t)}}
684+ popupWV.EXPECT().ID().Return(port.WebViewID(703)).Once()
685+ factory := mocks.NewMockWebViewFactory(t)
686+ factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once()
687+
688+ pm := newPopupManager()
689+ pm.setConfig(factory, nil, nil)
690+ browserCalls := 0
691+ pm.setOnOpenBrowserWindow(func(_ context.Context, input BrowserWindowInput) (BrowserWindowResult, error) {
692+ browserCalls++
693+ assert.Same(t, popupWV, input.PopupWebView)
694+ return BrowserWindowResult{WindowID: "fallback-window"}, nil
695+ })
696+ pm.setOnOpenNativePopup(func(ctx context.Context, input NativePopupInput) error {
697+ assert.True(t, input.AllowBrowserWindowFallback)
698+ assert.True(t, input.OnNativeHostAbort(ctx, input.PopupWebView))
699+ return nil
700+ })
701+
702+ got := pm.openNativePopup(ctx, popupCoordinatorHooks{}, "floating", 101, "", port.PopupRequest{
703+ TargetURI: "https://example.com/visual", NoJavaScriptAccess: false,
704+ }, dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})
705+
706+ require.Same(t, popupWV, got)
707+ assert.Equal(t, 1, browserCalls)
708+}
709+
677710 func TestPopupNativeAbortDoesNotFallbackForAuthOrRequiredOpener(t *testing.T) {
678711 for _, tc := range []struct {
679712 name string
@@ -711,6 +744,9 @@ func TestPopupNativeAbortDoesNotFallbackForAuthOrRequiredOpener(t *testing.T) {
711744 }
712745
713746 func TestPopupNativeFallbackFailureLeavesDestructionToNativeHost(t *testing.T) {
747+ var output bytes.Buffer
748+ logger := zerolog.New(&output)
749+ ctx := logging.WithContext(context.Background(), logger)
714750 popupWV := &popupNavigationWebViewStub{MockWebView: mocks.NewMockWebView(t)}
715751 popupWV.EXPECT().ID().Return(port.WebViewID(702)).Once()
716752 popupWV.EXPECT().Destroy().Once()
@@ -718,6 +754,7 @@ func TestPopupNativeFallbackFailureLeavesDestructionToNativeHost(t *testing.T) {
718754 factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once()
719755 pm := newPopupManager()
720756 pm.setConfig(factory, nil, nil)
757+ pm.setSourceHostResolver(func(entity.PaneID) dto.SourceHostKind { return dto.SourceHostFloating })
721758 pm.setOnOpenBrowserWindow(func(context.Context, BrowserWindowInput) (BrowserWindowResult, error) {
722759 return BrowserWindowResult{}, errors.New("fallback failed")
723760 })
@@ -728,10 +765,17 @@ func TestPopupNativeFallbackFailureLeavesDestructionToNativeHost(t *testing.T) {
728765 return nil
729766 })
730767
731- got := pm.openNativePopup(context.Background(), popupCoordinatorHooks{}, "floating", 101, "", port.PopupRequest{
732- TargetURI: "https://example.com/visual", NoJavaScriptAccess: true,
768+ got := pm.openNativePopup(ctx, popupCoordinatorHooks{}, "floating", 101, "", port.PopupRequest{
769+ Engine: dto.BrowserEngineCEF, TargetURI: "https://example.com/visual", NoJavaScriptAccess: true,
770+ TargetDisposition: dto.WindowDispositionNewPopup,
733771 }, dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})
734772 require.Same(t, popupWV, got)
773+ record := decodeBrowsingContextLog(t, output.Bytes())
774+ assert.Equal(t, "cef", record["engine"])
775+ assert.Equal(t, "floating", record["source_host"])
776+ assert.Equal(t, "create-native-popup", record["decision"])
777+ assert.Equal(t, "new-popup", record["target_disposition"])
778+ assert.Equal(t, "fallback-failed", record["reason_code"])
735779 }
736780
737781 func TestPopupNativeSetupFailureDestroysCoordinatorOwnedWebViewOnce(t *testing.T) {
@@ -1054,6 +1098,15 @@ func TestUpdatePopupConfig_CopiesLatestValue(t *testing.T) {
10541098 assert.False(t, initial.OAuthAutoClose)
10551099 }
10561100
1101+type popupOpenerWebViewStub struct {
1102+ *popupNavigationWebViewStub
1103+}
1104+
1105+func (*popupOpenerWebViewStub) EnablePopupOpenerBridge(port.WebView, bool) {}
1106+func (*popupOpenerWebViewStub) AddOpenerMessageCallback(func()) {}
1107+func (*popupOpenerWebViewStub) AddOpenerNavigationCallback(func(string)) {}
1108+func (*popupOpenerWebViewStub) HasActivePopupOpenerBridge() bool { return true }
1109+
10571110 type popupNavigationWebViewStub struct {
10581111 *mocks.MockWebView
10591112 primed []string
@@ -1,11 +1,14 @@
1 package content1 package content
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "errors"6 "errors"
6 "sync"7 "sync"
7 "testing"8 "testing"
8 9
10+ "github.com/rs/zerolog"
11+
9 "github.com/stretchr/testify/assert"12 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/mock"13 "github.com/stretchr/testify/mock"
11 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
@@ -15,6 +18,7 @@ import (
15 "github.com/bnema/dumber/internal/application/port/mocks"18 "github.com/bnema/dumber/internal/application/port/mocks"
16 "github.com/bnema/dumber/internal/domain/entity"19 "github.com/bnema/dumber/internal/domain/entity"
17 domainerrors "github.com/bnema/dumber/internal/domain/errors"20 domainerrors "github.com/bnema/dumber/internal/domain/errors"
21+ "github.com/bnema/dumber/internal/logging"
18 )22 )
19 23
20 // ---------------------------------------------------------------------------24 // ---------------------------------------------------------------------------
@@ -674,6 +678,35 @@ func TestPopupNativeAbortFallsBackOnceForNoOpenerVisualPopup(t *testing.T) {
674 assert.Equal(t, 1, browserCalls)678 assert.Equal(t, 1, browserCalls)
675 }679 }
676 680
681+func TestPopupNativeAbortFallsBackForOpenerCapableVisualPopup(t *testing.T) {
682+ ctx := context.Background()
683+ popupWV := &popupOpenerWebViewStub{popupNavigationWebViewStub: &popupNavigationWebViewStub{MockWebView: mocks.NewMockWebView(t)}}
684+ popupWV.EXPECT().ID().Return(port.WebViewID(703)).Once()
685+ factory := mocks.NewMockWebViewFactory(t)
686+ factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once()
687+
688+ pm := newPopupManager()
689+ pm.setConfig(factory, nil, nil)
690+ browserCalls := 0
691+ pm.setOnOpenBrowserWindow(func(_ context.Context, input BrowserWindowInput) (BrowserWindowResult, error) {
692+ browserCalls++
693+ assert.Same(t, popupWV, input.PopupWebView)
694+ return BrowserWindowResult{WindowID: "fallback-window"}, nil
695+ })
696+ pm.setOnOpenNativePopup(func(ctx context.Context, input NativePopupInput) error {
697+ assert.True(t, input.AllowBrowserWindowFallback)
698+ assert.True(t, input.OnNativeHostAbort(ctx, input.PopupWebView))
699+ return nil
700+ })
701+
702+ got := pm.openNativePopup(ctx, popupCoordinatorHooks{}, "floating", 101, "", port.PopupRequest{
703+ TargetURI: "https://example.com/visual", NoJavaScriptAccess: false,
704+ }, dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})
705+
706+ require.Same(t, popupWV, got)
707+ assert.Equal(t, 1, browserCalls)
708+}
709+
677 func TestPopupNativeAbortDoesNotFallbackForAuthOrRequiredOpener(t *testing.T) {710 func TestPopupNativeAbortDoesNotFallbackForAuthOrRequiredOpener(t *testing.T) {
678 for _, tc := range []struct {711 for _, tc := range []struct {
679 name string712 name string
@@ -711,6 +744,9 @@ func TestPopupNativeAbortDoesNotFallbackForAuthOrRequiredOpener(t *testing.T) {
711 }744 }
712 745
713 func TestPopupNativeFallbackFailureLeavesDestructionToNativeHost(t *testing.T) {746 func TestPopupNativeFallbackFailureLeavesDestructionToNativeHost(t *testing.T) {
747+ var output bytes.Buffer
748+ logger := zerolog.New(&output)
749+ ctx := logging.WithContext(context.Background(), logger)
714 popupWV := &popupNavigationWebViewStub{MockWebView: mocks.NewMockWebView(t)}750 popupWV := &popupNavigationWebViewStub{MockWebView: mocks.NewMockWebView(t)}
715 popupWV.EXPECT().ID().Return(port.WebViewID(702)).Once()751 popupWV.EXPECT().ID().Return(port.WebViewID(702)).Once()
716 popupWV.EXPECT().Destroy().Once()752 popupWV.EXPECT().Destroy().Once()
@@ -718,6 +754,7 @@ func TestPopupNativeFallbackFailureLeavesDestructionToNativeHost(t *testing.T) {
718 factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once()754 factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once()
719 pm := newPopupManager()755 pm := newPopupManager()
720 pm.setConfig(factory, nil, nil)756 pm.setConfig(factory, nil, nil)
757+ pm.setSourceHostResolver(func(entity.PaneID) dto.SourceHostKind { return dto.SourceHostFloating })
721 pm.setOnOpenBrowserWindow(func(context.Context, BrowserWindowInput) (BrowserWindowResult, error) {758 pm.setOnOpenBrowserWindow(func(context.Context, BrowserWindowInput) (BrowserWindowResult, error) {
722 return BrowserWindowResult{}, errors.New("fallback failed")759 return BrowserWindowResult{}, errors.New("fallback failed")
723 })760 })
@@ -728,10 +765,17 @@ func TestPopupNativeFallbackFailureLeavesDestructionToNativeHost(t *testing.T) {
728 return nil765 return nil
729 })766 })
730 767
731- got := pm.openNativePopup(context.Background(), popupCoordinatorHooks{}, "floating", 101, "", port.PopupRequest{768+ got := pm.openNativePopup(ctx, popupCoordinatorHooks{}, "floating", 101, "", port.PopupRequest{
732- TargetURI: "https://example.com/visual", NoJavaScriptAccess: true,769+ Engine: dto.BrowserEngineCEF, TargetURI: "https://example.com/visual", NoJavaScriptAccess: true,
770+ TargetDisposition: dto.WindowDispositionNewPopup,
733 }, dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})771 }, dto.HostDecision{Kind: dto.HostDecisionCreateNativePopup})
734 require.Same(t, popupWV, got)772 require.Same(t, popupWV, got)
773+ record := decodeBrowsingContextLog(t, output.Bytes())
774+ assert.Equal(t, "cef", record["engine"])
775+ assert.Equal(t, "floating", record["source_host"])
776+ assert.Equal(t, "create-native-popup", record["decision"])
777+ assert.Equal(t, "new-popup", record["target_disposition"])
778+ assert.Equal(t, "fallback-failed", record["reason_code"])
735 }779 }
736 780
737 func TestPopupNativeSetupFailureDestroysCoordinatorOwnedWebViewOnce(t *testing.T) {781 func TestPopupNativeSetupFailureDestroysCoordinatorOwnedWebViewOnce(t *testing.T) {
@@ -1054,6 +1098,15 @@ func TestUpdatePopupConfig_CopiesLatestValue(t *testing.T) {
1054 assert.False(t, initial.OAuthAutoClose)1098 assert.False(t, initial.OAuthAutoClose)
1055 }1099 }
1056 1100
1101+type popupOpenerWebViewStub struct {
1102+ *popupNavigationWebViewStub
1103+}
1104+
1105+func (*popupOpenerWebViewStub) EnablePopupOpenerBridge(port.WebView, bool) {}
1106+func (*popupOpenerWebViewStub) AddOpenerMessageCallback(func()) {}
1107+func (*popupOpenerWebViewStub) AddOpenerNavigationCallback(func(string)) {}
1108+func (*popupOpenerWebViewStub) HasActivePopupOpenerBridge() bool { return true }
1109+
1057 type popupNavigationWebViewStub struct {1110 type popupNavigationWebViewStub struct {
1058 *mocks.MockWebView1111 *mocks.MockWebView
1059 primed []string1112 primed []string

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