Separate browsing context DTOs and harden popup lifecycle
Reason: The browsing-context seam was still carrying concrete request and decision types inside the port package even though this branch now depends on those types across UI and engine integrations. At the same time, the remaining native-popup follow-ups still needed tighter lifecycle handling around OAuth auto-close scheduling and popup shell setup failures. Scope: - move browsing-context request and decision types into internal/application/dto - keep internal/application/port/browsing_context.go interface-only and update dependent signatures/usages - harden native popup auth close scheduling and popup setup failure cleanup - tighten popup regression tests and rerun mockery with the repo config Description: This commit introduces dto-based browsing-context request and decision types, rewires the CEF, WebKit, and coordinator call sites to use them, and leaves the port seam as interfaces only. It also makes native popup OAuth close scheduling one-shot, destroys popup webviews when popup shell creation fails before attach, and strengthens popup tests so the intended native branch is asserted explicitly.
- commit
- d0ff807740a4209881cfc244991407bb5c4a025c
- author
- brice · · 2026-05-13 09:10 UTC
- parent
- cf66830
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
16 files · +303 -211
A internal/application/dto/browsing_context.go +84 -0
| @@ -0,0 +1,84 @@ | |||
| 1 | + | package dto | |
| 2 | + | ||
| 3 | + | // WindowDisposition describes how the browsing context should be presented | |
| 4 | + | // relative to the source frame. Values mirror Chromium/CEF/WebKit navigation | |
| 5 | + | // dispositions at a normalized product seam. | |
| 6 | + | type WindowDisposition string | |
| 7 | + | ||
| 8 | + | const ( | |
| 9 | + | // WindowDispositionCurrentTab replaces the current document in the same browsing context. | |
| 10 | + | WindowDispositionCurrentTab WindowDisposition = "current-tab" | |
| 11 | + | // WindowDispositionNewTab opens in a new tab-like context. | |
| 12 | + | WindowDispositionNewTab WindowDisposition = "new-tab" | |
| 13 | + | // WindowDispositionNewPopup opens in a popup-style context. | |
| 14 | + | WindowDispositionNewPopup WindowDisposition = "new-popup" | |
| 15 | + | // WindowDispositionNewWindow opens in a new top-level window. | |
| 16 | + | WindowDispositionNewWindow WindowDisposition = "new-window" | |
| 17 | + | ) | |
| 18 | + | ||
| 19 | + | // TriggerKind categorizes what triggered a browsing-context request. | |
| 20 | + | type TriggerKind string | |
| 21 | + | ||
| 22 | + | const ( | |
| 23 | + | TriggerUnknown TriggerKind = "unknown" | |
| 24 | + | TriggerLinkNewPage TriggerKind = "link-new-page" | |
| 25 | + | TriggerScriptWindowOpen TriggerKind = "script-window-open" | |
| 26 | + | TriggerNamedTargetNavigation TriggerKind = "named-target-navigation" | |
| 27 | + | TriggerAuthPopupRequest TriggerKind = "auth-popup-request" | |
| 28 | + | ) | |
| 29 | + | ||
| 30 | + | // RequestContextDisposition makes request-context inheritance explicit at the | |
| 31 | + | // policy seam without leaking infrastructure request-context types upward. | |
| 32 | + | type RequestContextDisposition string | |
| 33 | + | ||
| 34 | + | const ( | |
| 35 | + | // RequestContextInheritParent preserves the parent request context/session. | |
| 36 | + | RequestContextInheritParent RequestContextDisposition = "inherit-parent" | |
| 37 | + | // RequestContextIsolate starts from a fresh unrelated request context. | |
| 38 | + | RequestContextIsolate RequestContextDisposition = "isolate" | |
| 39 | + | ) | |
| 40 | + | ||
| 41 | + | // NewBrowsingContextRequest is the normalized request captured from engine/UI | |
| 42 | + | // signals before policy decides how it should be hosted. | |
| 43 | + | type NewBrowsingContextRequest struct { | |
| 44 | + | ParentWebViewID uint64 | |
| 45 | + | ||
| 46 | + | SourceBrowserID int32 | |
| 47 | + | SourceFrameID string | |
| 48 | + | SourceFrameURL string | |
| 49 | + | ||
| 50 | + | TargetURI string | |
| 51 | + | TargetFrameName string | |
| 52 | + | TargetDisposition WindowDisposition | |
| 53 | + | ||
| 54 | + | IsUserGesture bool | |
| 55 | + | NoJavaScriptAccess bool | |
| 56 | + | WindowFeatures string | |
| 57 | + | ||
| 58 | + | TriggerKind TriggerKind | |
| 59 | + | AuthIntent bool | |
| 60 | + | RequiresNativeOpener bool | |
| 61 | + | RequestContextDisposition RequestContextDisposition | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // HostDecisionKind categorizes the approved host choices for a new browsing context. | |
| 65 | + | type HostDecisionKind string | |
| 66 | + | ||
| 67 | + | const ( | |
| 68 | + | HostDecisionReuseNamedPane HostDecisionKind = "reuse-named-pane" | |
| 69 | + | HostDecisionCreatePane HostDecisionKind = "create-pane" | |
| 70 | + | HostDecisionCreateNativeWin HostDecisionKind = "create-native-window" | |
| 71 | + | HostDecisionDeny HostDecisionKind = "deny" | |
| 72 | + | ) | |
| 73 | + | ||
| 74 | + | // HostDecision represents the output of browsing-context policy. | |
| 75 | + | type HostDecision struct { | |
| 76 | + | Kind HostDecisionKind | |
| 77 | + | ||
| 78 | + | ReuseContextName string | |
| 79 | + | BrowsingContextName string | |
| 80 | + | ||
| 81 | + | RequestContextDisposition RequestContextDisposition | |
| 82 | + | RequiresNativeOpener bool | |
| 83 | + | Reason string | |
| 84 | + | } | |
| @@ -0,0 +1,84 @@ | |||||
| 1 | + | package dto | |||
| 2 | + | ||||
| 3 | + | // WindowDisposition describes how the browsing context should be presented | |||
| 4 | + | // relative to the source frame. Values mirror Chromium/CEF/WebKit navigation | |||
| 5 | + | // dispositions at a normalized product seam. | |||
| 6 | + | type WindowDisposition string | |||
| 7 | + | ||||
| 8 | + | const ( | |||
| 9 | + | // WindowDispositionCurrentTab replaces the current document in the same browsing context. | |||
| 10 | + | WindowDispositionCurrentTab WindowDisposition = "current-tab" | |||
| 11 | + | // WindowDispositionNewTab opens in a new tab-like context. | |||
| 12 | + | WindowDispositionNewTab WindowDisposition = "new-tab" | |||
| 13 | + | // WindowDispositionNewPopup opens in a popup-style context. | |||
| 14 | + | WindowDispositionNewPopup WindowDisposition = "new-popup" | |||
| 15 | + | // WindowDispositionNewWindow opens in a new top-level window. | |||
| 16 | + | WindowDispositionNewWindow WindowDisposition = "new-window" | |||
| 17 | + | ) | |||
| 18 | + | ||||
| 19 | + | // TriggerKind categorizes what triggered a browsing-context request. | |||
| 20 | + | type TriggerKind string | |||
| 21 | + | ||||
| 22 | + | const ( | |||
| 23 | + | TriggerUnknown TriggerKind = "unknown" | |||
| 24 | + | TriggerLinkNewPage TriggerKind = "link-new-page" | |||
| 25 | + | TriggerScriptWindowOpen TriggerKind = "script-window-open" | |||
| 26 | + | TriggerNamedTargetNavigation TriggerKind = "named-target-navigation" | |||
| 27 | + | TriggerAuthPopupRequest TriggerKind = "auth-popup-request" | |||
| 28 | + | ) | |||
| 29 | + | ||||
| 30 | + | // RequestContextDisposition makes request-context inheritance explicit at the | |||
| 31 | + | // policy seam without leaking infrastructure request-context types upward. | |||
| 32 | + | type RequestContextDisposition string | |||
| 33 | + | ||||
| 34 | + | const ( | |||
| 35 | + | // RequestContextInheritParent preserves the parent request context/session. | |||
| 36 | + | RequestContextInheritParent RequestContextDisposition = "inherit-parent" | |||
| 37 | + | // RequestContextIsolate starts from a fresh unrelated request context. | |||
| 38 | + | RequestContextIsolate RequestContextDisposition = "isolate" | |||
| 39 | + | ) | |||
| 40 | + | ||||
| 41 | + | // NewBrowsingContextRequest is the normalized request captured from engine/UI | |||
| 42 | + | // signals before policy decides how it should be hosted. | |||
| 43 | + | type NewBrowsingContextRequest struct { | |||
| 44 | + | ParentWebViewID uint64 | |||
| 45 | + | ||||
| 46 | + | SourceBrowserID int32 | |||
| 47 | + | SourceFrameID string | |||
| 48 | + | SourceFrameURL string | |||
| 49 | + | ||||
| 50 | + | TargetURI string | |||
| 51 | + | TargetFrameName string | |||
| 52 | + | TargetDisposition WindowDisposition | |||
| 53 | + | ||||
| 54 | + | IsUserGesture bool | |||
| 55 | + | NoJavaScriptAccess bool | |||
| 56 | + | WindowFeatures string | |||
| 57 | + | ||||
| 58 | + | TriggerKind TriggerKind | |||
| 59 | + | AuthIntent bool | |||
| 60 | + | RequiresNativeOpener bool | |||
| 61 | + | RequestContextDisposition RequestContextDisposition | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // HostDecisionKind categorizes the approved host choices for a new browsing context. | |||
| 65 | + | type HostDecisionKind string | |||
| 66 | + | ||||
| 67 | + | const ( | |||
| 68 | + | HostDecisionReuseNamedPane HostDecisionKind = "reuse-named-pane" | |||
| 69 | + | HostDecisionCreatePane HostDecisionKind = "create-pane" | |||
| 70 | + | HostDecisionCreateNativeWin HostDecisionKind = "create-native-window" | |||
| 71 | + | HostDecisionDeny HostDecisionKind = "deny" | |||
| 72 | + | ) | |||
| 73 | + | ||||
| 74 | + | // HostDecision represents the output of browsing-context policy. | |||
| 75 | + | type HostDecision struct { | |||
| 76 | + | Kind HostDecisionKind | |||
| 77 | + | ||||
| 78 | + | ReuseContextName string | |||
| 79 | + | BrowsingContextName string | |||
| 80 | + | ||||
| 81 | + | RequestContextDisposition RequestContextDisposition | |||
| 82 | + | RequiresNativeOpener bool | |||
| 83 | + | Reason string | |||
| 84 | + | } | |||
M internal/application/port/browsing_context.go +4 -87
| @@ -1,91 +1,8 @@ | |||
| 1 | 1 | // Package port defines interfaces (ports) for use cases to depend on. | |
| 2 | - | // These types are the active seam for the approved browsing-context architecture | |
| 3 | - | // shared across policy, UI, and engine integrations. | |
| 4 | - | ||
| 2 | + | // Concrete browsing-context DTOs live in internal/application/dto. | |
| 5 | 3 | package port | |
| 6 | 4 | ||
| 7 | - | // WindowDisposition describes how the browsing context should be presented | |
| 8 | - | // relative to the source frame. Values mirror Chromium/CEF/WebKit navigation | |
| 9 | - | // dispositions at a normalized product seam. | |
| 10 | - | type WindowDisposition string | |
| 11 | - | ||
| 12 | - | const ( | |
| 13 | - | // WindowDispositionCurrentTab replaces the current document in the same browsing context. | |
| 14 | - | WindowDispositionCurrentTab WindowDisposition = "current-tab" | |
| 15 | - | // WindowDispositionNewTab opens in a new tab-like context. | |
| 16 | - | WindowDispositionNewTab WindowDisposition = "new-tab" | |
| 17 | - | // WindowDispositionNewPopup opens in a popup-style context. | |
| 18 | - | WindowDispositionNewPopup WindowDisposition = "new-popup" | |
| 19 | - | // WindowDispositionNewWindow opens in a new top-level window. | |
| 20 | - | WindowDispositionNewWindow WindowDisposition = "new-window" | |
| 21 | - | ) | |
| 22 | - | ||
| 23 | - | // TriggerKind categorizes what triggered a browsing-context request. | |
| 24 | - | type TriggerKind string | |
| 25 | - | ||
| 26 | - | const ( | |
| 27 | - | TriggerUnknown TriggerKind = "unknown" | |
| 28 | - | TriggerLinkNewPage TriggerKind = "link-new-page" | |
| 29 | - | TriggerScriptWindowOpen TriggerKind = "script-window-open" | |
| 30 | - | TriggerNamedTargetNavigation TriggerKind = "named-target-navigation" | |
| 31 | - | TriggerAuthPopupRequest TriggerKind = "auth-popup-request" | |
| 32 | - | ) | |
| 33 | - | ||
| 34 | - | // RequestContextDisposition makes request-context inheritance explicit at the | |
| 35 | - | // policy seam without leaking infrastructure request-context types upward. | |
| 36 | - | type RequestContextDisposition string | |
| 37 | - | ||
| 38 | - | const ( | |
| 39 | - | // RequestContextInheritParent preserves the parent request context/session. | |
| 40 | - | RequestContextInheritParent RequestContextDisposition = "inherit-parent" | |
| 41 | - | // RequestContextIsolate starts from a fresh unrelated request context. | |
| 42 | - | RequestContextIsolate RequestContextDisposition = "isolate" | |
| 43 | - | ) | |
| 44 | - | ||
| 45 | - | // NewBrowsingContextRequest is the normalized request captured from engine/UI | |
| 46 | - | // signals before policy decides how it should be hosted. | |
| 47 | - | type NewBrowsingContextRequest struct { | |
| 48 | - | ParentWebViewID WebViewID | |
| 49 | - | ||
| 50 | - | SourceBrowserID int32 | |
| 51 | - | SourceFrameID string | |
| 52 | - | SourceFrameURL string | |
| 53 | - | ||
| 54 | - | TargetURI string | |
| 55 | - | TargetFrameName string | |
| 56 | - | TargetDisposition WindowDisposition | |
| 57 | - | ||
| 58 | - | IsUserGesture bool | |
| 59 | - | NoJavaScriptAccess bool | |
| 60 | - | WindowFeatures string | |
| 61 | - | ||
| 62 | - | TriggerKind TriggerKind | |
| 63 | - | AuthIntent bool | |
| 64 | - | RequiresNativeOpener bool | |
| 65 | - | RequestContextDisposition RequestContextDisposition | |
| 66 | - | } | |
| 67 | - | ||
| 68 | - | // HostDecisionKind categorizes the approved host choices for a new browsing context. | |
| 69 | - | type HostDecisionKind string | |
| 70 | - | ||
| 71 | - | const ( | |
| 72 | - | HostDecisionReuseNamedPane HostDecisionKind = "reuse-named-pane" | |
| 73 | - | HostDecisionCreatePane HostDecisionKind = "create-pane" | |
| 74 | - | HostDecisionCreateNativeWin HostDecisionKind = "create-native-window" | |
| 75 | - | HostDecisionDeny HostDecisionKind = "deny" | |
| 76 | - | ) | |
| 77 | - | ||
| 78 | - | // HostDecision represents the output of browsing-context policy. | |
| 79 | - | type HostDecision struct { | |
| 80 | - | Kind HostDecisionKind | |
| 81 | - | ||
| 82 | - | ReuseContextName string | |
| 83 | - | BrowsingContextName string | |
| 84 | - | ||
| 85 | - | RequestContextDisposition RequestContextDisposition | |
| 86 | - | RequiresNativeOpener bool | |
| 87 | - | Reason string | |
| 88 | - | } | |
| 5 | + | import "github.com/bnema/dumber/internal/application/dto" | |
| 89 | 6 | ||
| 90 | 7 | // PaneHostedBrowsingContextCapable is implemented by engines that need an | |
| 91 | 8 | // explicit transition from a related/native-popup shell into a pane-hosted | |
| @@ -98,8 +15,8 @@ type PaneHostedBrowsingContextCapable interface { | |||
| 98 | 15 | // host decision to an engine webview so lower-level popup handlers can honor | |
| 99 | 16 | // pane-vs-native behavior synchronously. | |
| 100 | 17 | type BrowsingContextHostDecisionCapable interface { | |
| 101 | - | SetBrowsingContextHostDecision(decision HostDecision) | |
| 102 | - | BrowsingContextHostDecision() (HostDecision, bool) | |
| 18 | + | SetBrowsingContextHostDecision(decision dto.HostDecision) | |
| 19 | + | BrowsingContextHostDecision() (dto.HostDecision, bool) | |
| 103 | 20 | } | |
| 104 | 21 | ||
| 105 | 22 | // NativePopupHostAbortCapable lets the engine-level popup seam abort an | |
| @@ -1,91 +1,8 @@ | |||||
| 1 | // Package port defines interfaces (ports) for use cases to depend on. | 1 | // Package port defines interfaces (ports) for use cases to depend on. | ||
| 2 | - | // These types are the active seam for the approved browsing-context architecture | 2 | + | // Concrete browsing-context DTOs live in internal/application/dto. |
| 3 | - | // shared across policy, UI, and engine integrations. | |||
| 4 | - | ||||
| 5 | package port | 3 | package port | ||
| 6 | 4 | ||||
| 7 | - | // WindowDisposition describes how the browsing context should be presented | 5 | + | import "github.com/bnema/dumber/internal/application/dto" |
| 8 | - | // relative to the source frame. Values mirror Chromium/CEF/WebKit navigation | |||
| 9 | - | // dispositions at a normalized product seam. | |||
| 10 | - | type WindowDisposition string | |||
| 11 | - | ||||
| 12 | - | const ( | |||
| 13 | - | // WindowDispositionCurrentTab replaces the current document in the same browsing context. | |||
| 14 | - | WindowDispositionCurrentTab WindowDisposition = "current-tab" | |||
| 15 | - | // WindowDispositionNewTab opens in a new tab-like context. | |||
| 16 | - | WindowDispositionNewTab WindowDisposition = "new-tab" | |||
| 17 | - | // WindowDispositionNewPopup opens in a popup-style context. | |||
| 18 | - | WindowDispositionNewPopup WindowDisposition = "new-popup" | |||
| 19 | - | // WindowDispositionNewWindow opens in a new top-level window. | |||
| 20 | - | WindowDispositionNewWindow WindowDisposition = "new-window" | |||
| 21 | - | ) | |||
| 22 | - | ||||
| 23 | - | // TriggerKind categorizes what triggered a browsing-context request. | |||
| 24 | - | type TriggerKind string | |||
| 25 | - | ||||
| 26 | - | const ( | |||
| 27 | - | TriggerUnknown TriggerKind = "unknown" | |||
| 28 | - | TriggerLinkNewPage TriggerKind = "link-new-page" | |||
| 29 | - | TriggerScriptWindowOpen TriggerKind = "script-window-open" | |||
| 30 | - | TriggerNamedTargetNavigation TriggerKind = "named-target-navigation" | |||
| 31 | - | TriggerAuthPopupRequest TriggerKind = "auth-popup-request" | |||
| 32 | - | ) | |||
| 33 | - | ||||
| 34 | - | // RequestContextDisposition makes request-context inheritance explicit at the | |||
| 35 | - | // policy seam without leaking infrastructure request-context types upward. | |||
| 36 | - | type RequestContextDisposition string | |||
| 37 | - | ||||
| 38 | - | const ( | |||
| 39 | - | // RequestContextInheritParent preserves the parent request context/session. | |||
| 40 | - | RequestContextInheritParent RequestContextDisposition = "inherit-parent" | |||
| 41 | - | // RequestContextIsolate starts from a fresh unrelated request context. | |||
| 42 | - | RequestContextIsolate RequestContextDisposition = "isolate" | |||
| 43 | - | ) | |||
| 44 | - | ||||
| 45 | - | // NewBrowsingContextRequest is the normalized request captured from engine/UI | |||
| 46 | - | // signals before policy decides how it should be hosted. | |||
| 47 | - | type NewBrowsingContextRequest struct { | |||
| 48 | - | ParentWebViewID WebViewID | |||
| 49 | - | ||||
| 50 | - | SourceBrowserID int32 | |||
| 51 | - | SourceFrameID string | |||
| 52 | - | SourceFrameURL string | |||
| 53 | - | ||||
| 54 | - | TargetURI string | |||
| 55 | - | TargetFrameName string | |||
| 56 | - | TargetDisposition WindowDisposition | |||
| 57 | - | ||||
| 58 | - | IsUserGesture bool | |||
| 59 | - | NoJavaScriptAccess bool | |||
| 60 | - | WindowFeatures string | |||
| 61 | - | ||||
| 62 | - | TriggerKind TriggerKind | |||
| 63 | - | AuthIntent bool | |||
| 64 | - | RequiresNativeOpener bool | |||
| 65 | - | RequestContextDisposition RequestContextDisposition | |||
| 66 | - | } | |||
| 67 | - | ||||
| 68 | - | // HostDecisionKind categorizes the approved host choices for a new browsing context. | |||
| 69 | - | type HostDecisionKind string | |||
| 70 | - | ||||
| 71 | - | const ( | |||
| 72 | - | HostDecisionReuseNamedPane HostDecisionKind = "reuse-named-pane" | |||
| 73 | - | HostDecisionCreatePane HostDecisionKind = "create-pane" | |||
| 74 | - | HostDecisionCreateNativeWin HostDecisionKind = "create-native-window" | |||
| 75 | - | HostDecisionDeny HostDecisionKind = "deny" | |||
| 76 | - | ) | |||
| 77 | - | ||||
| 78 | - | // HostDecision represents the output of browsing-context policy. | |||
| 79 | - | type HostDecision struct { | |||
| 80 | - | Kind HostDecisionKind | |||
| 81 | - | ||||
| 82 | - | ReuseContextName string | |||
| 83 | - | BrowsingContextName string | |||
| 84 | - | ||||
| 85 | - | RequestContextDisposition RequestContextDisposition | |||
| 86 | - | RequiresNativeOpener bool | |||
| 87 | - | Reason string | |||
| 88 | - | } | |||
| 89 | 6 | ||||
| 90 | // PaneHostedBrowsingContextCapable is implemented by engines that need an | 7 | // PaneHostedBrowsingContextCapable is implemented by engines that need an | ||
| 91 | // explicit transition from a related/native-popup shell into a pane-hosted | 8 | // explicit transition from a related/native-popup shell into a pane-hosted | ||
| @@ -98,8 +15,8 @@ type PaneHostedBrowsingContextCapable interface { | |||||
| 98 | // host decision to an engine webview so lower-level popup handlers can honor | 15 | // host decision to an engine webview so lower-level popup handlers can honor | ||
| 99 | // pane-vs-native behavior synchronously. | 16 | // pane-vs-native behavior synchronously. | ||
| 100 | type BrowsingContextHostDecisionCapable interface { | 17 | type BrowsingContextHostDecisionCapable interface { | ||
| 101 | - | SetBrowsingContextHostDecision(decision HostDecision) | 18 | + | SetBrowsingContextHostDecision(decision dto.HostDecision) |
| 102 | - | BrowsingContextHostDecision() (HostDecision, bool) | 19 | + | BrowsingContextHostDecision() (dto.HostDecision, bool) |
| 103 | } | 20 | } | ||
| 104 | 21 | ||||
| 105 | // NativePopupHostAbortCapable lets the engine-level popup seam abort an | 22 | // NativePopupHostAbortCapable lets the engine-level popup seam abort an | ||
M internal/application/port/webview.go +6 -2
| @@ -3,7 +3,11 @@ | |||
| 3 | 3 | // remain independent of specific implementations (WebKit, GTK, etc.). | |
| 4 | 4 | package port | |
| 5 | 5 | ||
| 6 | - | import "context" | |
| 6 | + | import ( | |
| 7 | + | "context" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 10 | + | ) | |
| 7 | 11 | ||
| 8 | 12 | // WebViewID uniquely identifies a WebView instance. | |
| 9 | 13 | type WebViewID uint64 | |
| @@ -72,7 +76,7 @@ type PopupRequest struct { | |||
| 72 | 76 | SourceFrameID string | |
| 73 | 77 | SourceFrameURL string | |
| 74 | 78 | ||
| 75 | - | TargetDisposition WindowDisposition | |
| 79 | + | TargetDisposition dto.WindowDisposition | |
| 76 | 80 | WindowFeatures string | |
| 77 | 81 | } | |
| 78 | 82 | ||
| @@ -3,7 +3,11 @@ | |||||
| 3 | // remain independent of specific implementations (WebKit, GTK, etc.). | 3 | // remain independent of specific implementations (WebKit, GTK, etc.). | ||
| 4 | package port | 4 | package port | ||
| 5 | 5 | ||||
| 6 | - | import "context" | 6 | + | import ( |
| 7 | + | "context" | |||
| 8 | + | ||||
| 9 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 10 | + | ) | |||
| 7 | 11 | ||||
| 8 | // WebViewID uniquely identifies a WebView instance. | 12 | // WebViewID uniquely identifies a WebView instance. | ||
| 9 | type WebViewID uint64 | 13 | type WebViewID uint64 | ||
| @@ -72,7 +76,7 @@ type PopupRequest struct { | |||||
| 72 | SourceFrameID string | 76 | SourceFrameID string | ||
| 73 | SourceFrameURL string | 77 | SourceFrameURL string | ||
| 74 | 78 | ||||
| 75 | - | TargetDisposition WindowDisposition | 79 | + | TargetDisposition dto.WindowDisposition |
| 76 | WindowFeatures string | 80 | WindowFeatures string | ||
| 77 | } | 81 | } | ||
| 78 | 82 | ||||
M internal/infrastructure/cef/webview.go +5 -4
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | 18 | "github.com/bnema/puregotk/v4/glib" | |
| 19 | 19 | "github.com/bnema/puregotk/v4/gtk" | |
| 20 | 20 | ||
| 21 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 21 | 22 | "github.com/bnema/dumber/internal/application/port" | |
| 22 | 23 | "github.com/bnema/dumber/internal/logging" | |
| 23 | 24 | ) | |
| @@ -120,7 +121,7 @@ type WebView struct { | |||
| 120 | 121 | // Callbacks and browsing-context state set by the UI layer. | |
| 121 | 122 | mu sync.RWMutex | |
| 122 | 123 | callbacks *port.WebViewCallbacks | |
| 123 | - | browsingContextDecision port.HostDecision | |
| 124 | + | browsingContextDecision dto.HostDecision | |
| 124 | 125 | hasBrowsingContextDecision bool | |
| 125 | 126 | nativePopupHostAbort func() | |
| 126 | 127 | ||
| @@ -677,7 +678,7 @@ func (wv *WebView) PreparePaneHostedBrowsingContext() { | |||
| 677 | 678 | wv.discardNativePopupCandidate() | |
| 678 | 679 | } | |
| 679 | 680 | ||
| 680 | - | func (wv *WebView) SetBrowsingContextHostDecision(decision port.HostDecision) { | |
| 681 | + | func (wv *WebView) SetBrowsingContextHostDecision(decision dto.HostDecision) { | |
| 681 | 682 | if wv == nil { | |
| 682 | 683 | return | |
| 683 | 684 | } | |
| @@ -687,9 +688,9 @@ func (wv *WebView) SetBrowsingContextHostDecision(decision port.HostDecision) { | |||
| 687 | 688 | wv.mu.Unlock() | |
| 688 | 689 | } | |
| 689 | 690 | ||
| 690 | - | func (wv *WebView) BrowsingContextHostDecision() (port.HostDecision, bool) { | |
| 691 | + | func (wv *WebView) BrowsingContextHostDecision() (dto.HostDecision, bool) { | |
| 691 | 692 | if wv == nil { | |
| 692 | - | return port.HostDecision{}, false | |
| 693 | + | return dto.HostDecision{}, false | |
| 693 | 694 | } | |
| 694 | 695 | wv.mu.RLock() | |
| 695 | 696 | defer wv.mu.RUnlock() | |
| @@ -18,6 +18,7 @@ import ( | |||||
| 18 | "github.com/bnema/puregotk/v4/glib" | 18 | "github.com/bnema/puregotk/v4/glib" | ||
| 19 | "github.com/bnema/puregotk/v4/gtk" | 19 | "github.com/bnema/puregotk/v4/gtk" | ||
| 20 | 20 | ||||
| 21 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 21 | "github.com/bnema/dumber/internal/application/port" | 22 | "github.com/bnema/dumber/internal/application/port" | ||
| 22 | "github.com/bnema/dumber/internal/logging" | 23 | "github.com/bnema/dumber/internal/logging" | ||
| 23 | ) | 24 | ) | ||
| @@ -120,7 +121,7 @@ type WebView struct { | |||||
| 120 | // Callbacks and browsing-context state set by the UI layer. | 121 | // Callbacks and browsing-context state set by the UI layer. | ||
| 121 | mu sync.RWMutex | 122 | mu sync.RWMutex | ||
| 122 | callbacks *port.WebViewCallbacks | 123 | callbacks *port.WebViewCallbacks | ||
| 123 | - | browsingContextDecision port.HostDecision | 124 | + | browsingContextDecision dto.HostDecision |
| 124 | hasBrowsingContextDecision bool | 125 | hasBrowsingContextDecision bool | ||
| 125 | nativePopupHostAbort func() | 126 | nativePopupHostAbort func() | ||
| 126 | 127 | ||||
| @@ -677,7 +678,7 @@ func (wv *WebView) PreparePaneHostedBrowsingContext() { | |||||
| 677 | wv.discardNativePopupCandidate() | 678 | wv.discardNativePopupCandidate() | ||
| 678 | } | 679 | } | ||
| 679 | 680 | ||||
| 680 | - | func (wv *WebView) SetBrowsingContextHostDecision(decision port.HostDecision) { | 681 | + | func (wv *WebView) SetBrowsingContextHostDecision(decision dto.HostDecision) { |
| 681 | if wv == nil { | 682 | if wv == nil { | ||
| 682 | return | 683 | return | ||
| 683 | } | 684 | } | ||
| @@ -687,9 +688,9 @@ func (wv *WebView) SetBrowsingContextHostDecision(decision port.HostDecision) { | |||||
| 687 | wv.mu.Unlock() | 688 | wv.mu.Unlock() | ||
| 688 | } | 689 | } | ||
| 689 | 690 | ||||
| 690 | - | func (wv *WebView) BrowsingContextHostDecision() (port.HostDecision, bool) { | 691 | + | func (wv *WebView) BrowsingContextHostDecision() (dto.HostDecision, bool) { |
| 691 | if wv == nil { | 692 | if wv == nil { | ||
| 692 | - | return port.HostDecision{}, false | 693 | + | return dto.HostDecision{}, false |
| 693 | } | 694 | } | ||
| 694 | wv.mu.RLock() | 695 | wv.mu.RLock() | ||
| 695 | defer wv.mu.RUnlock() | 696 | defer wv.mu.RUnlock() | ||
M internal/infrastructure/cef/webview_handlers.go +9 -8
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | purecef "github.com/bnema/purego-cef/cef" | |
| 9 | 9 | cef2gtk "github.com/bnema/purego-cef2gtk" | |
| 10 | 10 | ||
| 11 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 11 | 12 | "github.com/bnema/dumber/internal/application/port" | |
| 12 | 13 | downloadutil "github.com/bnema/dumber/internal/domain/download" | |
| 13 | 14 | "github.com/bnema/dumber/internal/logging" | |
| @@ -419,19 +420,19 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||
| 419 | 420 | func (h *handlerSet) OnLoadError(_ purecef.Browser, _ purecef.Frame, _ purecef.Errorcode, _, _ string) { | |
| 420 | 421 | } | |
| 421 | 422 | ||
| 422 | - | func mapCEFWindowDisposition(disposition purecef.WindowOpenDisposition) port.WindowDisposition { | |
| 423 | + | func mapCEFWindowDisposition(disposition purecef.WindowOpenDisposition) dto.WindowDisposition { | |
| 423 | 424 | switch disposition { | |
| 424 | 425 | case purecef.WindowOpenDispositionWodCurrentTab: | |
| 425 | - | return port.WindowDispositionCurrentTab | |
| 426 | + | return dto.WindowDispositionCurrentTab | |
| 426 | 427 | case purecef.WindowOpenDispositionWodNewPopup: | |
| 427 | - | return port.WindowDispositionNewPopup | |
| 428 | + | return dto.WindowDispositionNewPopup | |
| 428 | 429 | case purecef.WindowOpenDispositionWodNewWindow: | |
| 429 | - | return port.WindowDispositionNewWindow | |
| 430 | + | return dto.WindowDispositionNewWindow | |
| 430 | 431 | case purecef.WindowOpenDispositionWodNewForegroundTab, | |
| 431 | 432 | purecef.WindowOpenDispositionWodNewBackgroundTab, | |
| 432 | 433 | purecef.WindowOpenDispositionWodSingletonTab, | |
| 433 | 434 | purecef.WindowOpenDispositionWodSwitchToTab: | |
| 434 | - | return port.WindowDispositionNewTab | |
| 435 | + | return dto.WindowDispositionNewTab | |
| 435 | 436 | default: | |
| 436 | 437 | return "" | |
| 437 | 438 | } | |
| @@ -498,7 +499,7 @@ func (h *handlerSet) OnBeforePopup( | |||
| 498 | 499 | return true | |
| 499 | 500 | } | |
| 500 | 501 | ||
| 501 | - | decision := port.HostDecision{} | |
| 502 | + | decision := dto.HostDecision{} | |
| 502 | 503 | if carrier, ok := popup.(port.BrowsingContextHostDecisionCapable); ok { | |
| 503 | 504 | if got, hasDecision := carrier.BrowsingContextHostDecision(); hasDecision { | |
| 504 | 505 | decision = got | |
| @@ -507,7 +508,7 @@ func (h *handlerSet) OnBeforePopup( | |||
| 507 | 508 | ||
| 508 | 509 | cefPopup.setPopupNoJavaScriptAccess(requestNoJavaScriptAccess) | |
| 509 | 510 | switch decision.Kind { | |
| 510 | - | case port.HostDecisionCreateNativeWin: | |
| 511 | + | case dto.HostDecisionCreateNativeWin: | |
| 511 | 512 | if cefPopup.prepareNativePopup(popupID, targetURL, windowInfo, clientSlot, settings) { | |
| 512 | 513 | logging.FromContext(h.currentContext()).Debug(). | |
| 513 | 514 | Int32("popup_id", popupID). | |
| @@ -524,7 +525,7 @@ func (h *handlerSet) OnBeforePopup( | |||
| 524 | 525 | Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). | |
| 525 | 526 | Msg("cef: native popup arming failed, denying without fallback") | |
| 526 | 527 | return true | |
| 527 | - | case port.HostDecisionCreatePane, port.HostDecisionReuseNamedPane: | |
| 528 | + | case dto.HostDecisionCreatePane, dto.HostDecisionReuseNamedPane: | |
| 528 | 529 | logging.FromContext(h.currentContext()).Debug(). | |
| 529 | 530 | Int32("popup_id", popupID). | |
| 530 | 531 | Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | purecef "github.com/bnema/purego-cef/cef" | 8 | purecef "github.com/bnema/purego-cef/cef" | ||
| 9 | cef2gtk "github.com/bnema/purego-cef2gtk" | 9 | cef2gtk "github.com/bnema/purego-cef2gtk" | ||
| 10 | 10 | ||||
| 11 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 11 | "github.com/bnema/dumber/internal/application/port" | 12 | "github.com/bnema/dumber/internal/application/port" | ||
| 12 | downloadutil "github.com/bnema/dumber/internal/domain/download" | 13 | downloadutil "github.com/bnema/dumber/internal/domain/download" | ||
| 13 | "github.com/bnema/dumber/internal/logging" | 14 | "github.com/bnema/dumber/internal/logging" | ||
| @@ -419,19 +420,19 @@ func (h *handlerSet) OnLoadEnd(_ purecef.Browser, frame purecef.Frame, httpStatu | |||||
| 419 | func (h *handlerSet) OnLoadError(_ purecef.Browser, _ purecef.Frame, _ purecef.Errorcode, _, _ string) { | 420 | func (h *handlerSet) OnLoadError(_ purecef.Browser, _ purecef.Frame, _ purecef.Errorcode, _, _ string) { | ||
| 420 | } | 421 | } | ||
| 421 | 422 | ||||
| 422 | - | func mapCEFWindowDisposition(disposition purecef.WindowOpenDisposition) port.WindowDisposition { | 423 | + | func mapCEFWindowDisposition(disposition purecef.WindowOpenDisposition) dto.WindowDisposition { |
| 423 | switch disposition { | 424 | switch disposition { | ||
| 424 | case purecef.WindowOpenDispositionWodCurrentTab: | 425 | case purecef.WindowOpenDispositionWodCurrentTab: | ||
| 425 | - | return port.WindowDispositionCurrentTab | 426 | + | return dto.WindowDispositionCurrentTab |
| 426 | case purecef.WindowOpenDispositionWodNewPopup: | 427 | case purecef.WindowOpenDispositionWodNewPopup: | ||
| 427 | - | return port.WindowDispositionNewPopup | 428 | + | return dto.WindowDispositionNewPopup |
| 428 | case purecef.WindowOpenDispositionWodNewWindow: | 429 | case purecef.WindowOpenDispositionWodNewWindow: | ||
| 429 | - | return port.WindowDispositionNewWindow | 430 | + | return dto.WindowDispositionNewWindow |
| 430 | case purecef.WindowOpenDispositionWodNewForegroundTab, | 431 | case purecef.WindowOpenDispositionWodNewForegroundTab, | ||
| 431 | purecef.WindowOpenDispositionWodNewBackgroundTab, | 432 | purecef.WindowOpenDispositionWodNewBackgroundTab, | ||
| 432 | purecef.WindowOpenDispositionWodSingletonTab, | 433 | purecef.WindowOpenDispositionWodSingletonTab, | ||
| 433 | purecef.WindowOpenDispositionWodSwitchToTab: | 434 | purecef.WindowOpenDispositionWodSwitchToTab: | ||
| 434 | - | return port.WindowDispositionNewTab | 435 | + | return dto.WindowDispositionNewTab |
| 435 | default: | 436 | default: | ||
| 436 | return "" | 437 | return "" | ||
| 437 | } | 438 | } | ||
| @@ -498,7 +499,7 @@ func (h *handlerSet) OnBeforePopup( | |||||
| 498 | return true | 499 | return true | ||
| 499 | } | 500 | } | ||
| 500 | 501 | ||||
| 501 | - | decision := port.HostDecision{} | 502 | + | decision := dto.HostDecision{} |
| 502 | if carrier, ok := popup.(port.BrowsingContextHostDecisionCapable); ok { | 503 | if carrier, ok := popup.(port.BrowsingContextHostDecisionCapable); ok { | ||
| 503 | if got, hasDecision := carrier.BrowsingContextHostDecision(); hasDecision { | 504 | if got, hasDecision := carrier.BrowsingContextHostDecision(); hasDecision { | ||
| 504 | decision = got | 505 | decision = got | ||
| @@ -507,7 +508,7 @@ func (h *handlerSet) OnBeforePopup( | |||||
| 507 | 508 | ||||
| 508 | cefPopup.setPopupNoJavaScriptAccess(requestNoJavaScriptAccess) | 509 | cefPopup.setPopupNoJavaScriptAccess(requestNoJavaScriptAccess) | ||
| 509 | switch decision.Kind { | 510 | switch decision.Kind { | ||
| 510 | - | case port.HostDecisionCreateNativeWin: | 511 | + | case dto.HostDecisionCreateNativeWin: |
| 511 | if cefPopup.prepareNativePopup(popupID, targetURL, windowInfo, clientSlot, settings) { | 512 | if cefPopup.prepareNativePopup(popupID, targetURL, windowInfo, clientSlot, settings) { | ||
| 512 | logging.FromContext(h.currentContext()).Debug(). | 513 | logging.FromContext(h.currentContext()).Debug(). | ||
| 513 | Int32("popup_id", popupID). | 514 | Int32("popup_id", popupID). | ||
| @@ -524,7 +525,7 @@ func (h *handlerSet) OnBeforePopup( | |||||
| 524 | Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). | 525 | Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). | ||
| 525 | Msg("cef: native popup arming failed, denying without fallback") | 526 | Msg("cef: native popup arming failed, denying without fallback") | ||
| 526 | return true | 527 | return true | ||
| 527 | - | case port.HostDecisionCreatePane, port.HostDecisionReuseNamedPane: | 528 | + | case dto.HostDecisionCreatePane, dto.HostDecisionReuseNamedPane: |
| 528 | logging.FromContext(h.currentContext()).Debug(). | 529 | logging.FromContext(h.currentContext()).Debug(). | ||
| 529 | Int32("popup_id", popupID). | 530 | Int32("popup_id", popupID). | ||
| 530 | Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). | 531 | Str("target_url", logging.TruncateURL(targetURL, logging.PermissionLogURLMaxLen)). | ||
M internal/infrastructure/cef/webview_handlers_test.go +2 -2
| @@ -250,7 +250,7 @@ func TestOnBeforePopup_PrimesPopupShellWhenNativePopupCannotBeArmed(t *testing.T | |||
| 250 | 250 | func TestOnBeforePopup_PaneDecisionBlocksNativePopupAndKeepsPanePath(t *testing.T) { | |
| 251 | 251 | parentWV := &WebView{ctx: context.Background(), id: 41} | |
| 252 | 252 | popupWV := &WebView{ctx: context.Background(), id: 42} | |
| 253 | - | popupWV.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreatePane}) | |
| 253 | + | popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreatePane}) | |
| 254 | 254 | parentWV.SetCallbacks(&port.WebViewCallbacks{ | |
| 255 | 255 | OnCreate: func(req port.PopupRequest) port.WebView { | |
| 256 | 256 | popupWV.PreparePaneHostedBrowsingContext() | |
| @@ -270,7 +270,7 @@ func TestOnBeforePopup_NativeDecisionAbortsHostWhenArmingFails(t *testing.T) { | |||
| 270 | 270 | parentWV := &WebView{ctx: context.Background(), id: 51} | |
| 271 | 271 | popupWV := &WebView{ctx: context.Background(), id: 52} | |
| 272 | 272 | popupWV.markNativePopupCandidate(parentWV) | |
| 273 | - | popupWV.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreateNativeWin}) | |
| 273 | + | popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateNativeWin}) | |
| 274 | 274 | aborted := false | |
| 275 | 275 | popupWV.SetNativePopupHostAbort(func() { aborted = true }) | |
| 276 | 276 | parentWV.SetCallbacks(&port.WebViewCallbacks{ | |
| @@ -250,7 +250,7 @@ func TestOnBeforePopup_PrimesPopupShellWhenNativePopupCannotBeArmed(t *testing.T | |||||
| 250 | func TestOnBeforePopup_PaneDecisionBlocksNativePopupAndKeepsPanePath(t *testing.T) { | 250 | func TestOnBeforePopup_PaneDecisionBlocksNativePopupAndKeepsPanePath(t *testing.T) { | ||
| 251 | parentWV := &WebView{ctx: context.Background(), id: 41} | 251 | parentWV := &WebView{ctx: context.Background(), id: 41} | ||
| 252 | popupWV := &WebView{ctx: context.Background(), id: 42} | 252 | popupWV := &WebView{ctx: context.Background(), id: 42} | ||
| 253 | - | popupWV.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreatePane}) | 253 | + | popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreatePane}) |
| 254 | parentWV.SetCallbacks(&port.WebViewCallbacks{ | 254 | parentWV.SetCallbacks(&port.WebViewCallbacks{ | ||
| 255 | OnCreate: func(req port.PopupRequest) port.WebView { | 255 | OnCreate: func(req port.PopupRequest) port.WebView { | ||
| 256 | popupWV.PreparePaneHostedBrowsingContext() | 256 | popupWV.PreparePaneHostedBrowsingContext() | ||
| @@ -270,7 +270,7 @@ func TestOnBeforePopup_NativeDecisionAbortsHostWhenArmingFails(t *testing.T) { | |||||
| 270 | parentWV := &WebView{ctx: context.Background(), id: 51} | 270 | parentWV := &WebView{ctx: context.Background(), id: 51} | ||
| 271 | popupWV := &WebView{ctx: context.Background(), id: 52} | 271 | popupWV := &WebView{ctx: context.Background(), id: 52} | ||
| 272 | popupWV.markNativePopupCandidate(parentWV) | 272 | popupWV.markNativePopupCandidate(parentWV) | ||
| 273 | - | popupWV.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreateNativeWin}) | 273 | + | popupWV.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateNativeWin}) |
| 274 | aborted := false | 274 | aborted := false | ||
| 275 | popupWV.SetNativePopupHostAbort(func() { aborted = true }) | 275 | popupWV.SetNativePopupHostAbort(func() { aborted = true }) | ||
| 276 | parentWV.SetCallbacks(&port.WebViewCallbacks{ | 276 | parentWV.SetCallbacks(&port.WebViewCallbacks{ | ||
M internal/infrastructure/webkit/webview.go +6 -5
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "sync/atomic" | |
| 11 | 11 | "time" | |
| 12 | 12 | ||
| 13 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 13 | 14 | "github.com/bnema/dumber/internal/application/port" | |
| 14 | 15 | downloadutil "github.com/bnema/dumber/internal/domain/download" | |
| 15 | 16 | "github.com/bnema/dumber/internal/domain/entity" | |
| @@ -161,7 +162,7 @@ type WebView struct { | |||
| 161 | 162 | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | |
| 162 | 163 | OnLinkHover func(uri string) // Called when hovering over a link/image/media (empty string when leaving) | |
| 163 | 164 | OnWebProcessTerminated func(reason webkit.WebProcessTerminationReason, reasonLabel string, uri string) | |
| 164 | - | browsingContextDecision port.HostDecision | |
| 165 | + | browsingContextDecision dto.HostDecision | |
| 165 | 166 | hasBrowsingContextDecision bool | |
| 166 | 167 | nativePopupHostAbort func() | |
| 167 | 168 | ||
| @@ -521,14 +522,14 @@ func (wv *WebView) connectCloseSignal() { | |||
| 521 | 522 | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | |
| 522 | 523 | } | |
| 523 | 524 | ||
| 524 | - | func (wv *WebView) SetBrowsingContextHostDecision(decision port.HostDecision) { | |
| 525 | + | func (wv *WebView) SetBrowsingContextHostDecision(decision dto.HostDecision) { | |
| 525 | 526 | wv.mu.Lock() | |
| 526 | 527 | wv.browsingContextDecision = decision | |
| 527 | 528 | wv.hasBrowsingContextDecision = true | |
| 528 | 529 | wv.mu.Unlock() | |
| 529 | 530 | } | |
| 530 | 531 | ||
| 531 | - | func (wv *WebView) BrowsingContextHostDecision() (port.HostDecision, bool) { | |
| 532 | + | func (wv *WebView) BrowsingContextHostDecision() (dto.HostDecision, bool) { | |
| 532 | 533 | wv.mu.RLock() | |
| 533 | 534 | defer wv.mu.RUnlock() | |
| 534 | 535 | return wv.browsingContextDecision, wv.hasBrowsingContextDecision | |
| @@ -1927,7 +1928,7 @@ func (wv *WebView) DestroyWithPolicy(policy string) { | |||
| 1927 | 1928 | // 3. Clear async callback references and popup-hosting state | |
| 1928 | 1929 | wv.mu.Lock() | |
| 1929 | 1930 | wv.asyncCallbacks = nil | |
| 1930 | - | wv.browsingContextDecision = port.HostDecision{} | |
| 1931 | + | wv.browsingContextDecision = dto.HostDecision{} | |
| 1931 | 1932 | wv.hasBrowsingContextDecision = false | |
| 1932 | 1933 | wv.nativePopupHostAbort = nil | |
| 1933 | 1934 | wv.mu.Unlock() | |
| @@ -1997,7 +1998,7 @@ func (wv *WebView) ResetForPoolReuse() { | |||
| 1997 | 1998 | wv.isLoading = false | |
| 1998 | 1999 | wv.asyncCallbacks = nil | |
| 1999 | 2000 | wv.runJSErrorStats = make(map[string]runJSErrorStat) | |
| 2000 | - | wv.browsingContextDecision = port.HostDecision{} | |
| 2001 | + | wv.browsingContextDecision = dto.HostDecision{} | |
| 2001 | 2002 | wv.hasBrowsingContextDecision = false | |
| 2002 | 2003 | wv.nativePopupHostAbort = nil | |
| 2003 | 2004 | wv.lastProgressUpdate.Store(0) | |
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "sync/atomic" | 10 | "sync/atomic" | ||
| 11 | "time" | 11 | "time" | ||
| 12 | 12 | ||||
| 13 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 13 | "github.com/bnema/dumber/internal/application/port" | 14 | "github.com/bnema/dumber/internal/application/port" | ||
| 14 | downloadutil "github.com/bnema/dumber/internal/domain/download" | 15 | downloadutil "github.com/bnema/dumber/internal/domain/download" | ||
| 15 | "github.com/bnema/dumber/internal/domain/entity" | 16 | "github.com/bnema/dumber/internal/domain/entity" | ||
| @@ -161,7 +162,7 @@ type WebView struct { | |||||
| 161 | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | 162 | OnAudioStateChanged func(playing bool) // Called when audio playback starts/stops | ||
| 162 | OnLinkHover func(uri string) // Called when hovering over a link/image/media (empty string when leaving) | 163 | OnLinkHover func(uri string) // Called when hovering over a link/image/media (empty string when leaving) | ||
| 163 | OnWebProcessTerminated func(reason webkit.WebProcessTerminationReason, reasonLabel string, uri string) | 164 | OnWebProcessTerminated func(reason webkit.WebProcessTerminationReason, reasonLabel string, uri string) | ||
| 164 | - | browsingContextDecision port.HostDecision | 165 | + | browsingContextDecision dto.HostDecision |
| 165 | hasBrowsingContextDecision bool | 166 | hasBrowsingContextDecision bool | ||
| 166 | nativePopupHostAbort func() | 167 | nativePopupHostAbort func() | ||
| 167 | 168 | ||||
| @@ -521,14 +522,14 @@ func (wv *WebView) connectCloseSignal() { | |||||
| 521 | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | 522 | wv.signalIDs = append(wv.signalIDs, uintptr(sigID)) | ||
| 522 | } | 523 | } | ||
| 523 | 524 | ||||
| 524 | - | func (wv *WebView) SetBrowsingContextHostDecision(decision port.HostDecision) { | 525 | + | func (wv *WebView) SetBrowsingContextHostDecision(decision dto.HostDecision) { |
| 525 | wv.mu.Lock() | 526 | wv.mu.Lock() | ||
| 526 | wv.browsingContextDecision = decision | 527 | wv.browsingContextDecision = decision | ||
| 527 | wv.hasBrowsingContextDecision = true | 528 | wv.hasBrowsingContextDecision = true | ||
| 528 | wv.mu.Unlock() | 529 | wv.mu.Unlock() | ||
| 529 | } | 530 | } | ||
| 530 | 531 | ||||
| 531 | - | func (wv *WebView) BrowsingContextHostDecision() (port.HostDecision, bool) { | 532 | + | func (wv *WebView) BrowsingContextHostDecision() (dto.HostDecision, bool) { |
| 532 | wv.mu.RLock() | 533 | wv.mu.RLock() | ||
| 533 | defer wv.mu.RUnlock() | 534 | defer wv.mu.RUnlock() | ||
| 534 | return wv.browsingContextDecision, wv.hasBrowsingContextDecision | 535 | return wv.browsingContextDecision, wv.hasBrowsingContextDecision | ||
| @@ -1927,7 +1928,7 @@ func (wv *WebView) DestroyWithPolicy(policy string) { | |||||
| 1927 | // 3. Clear async callback references and popup-hosting state | 1928 | // 3. Clear async callback references and popup-hosting state | ||
| 1928 | wv.mu.Lock() | 1929 | wv.mu.Lock() | ||
| 1929 | wv.asyncCallbacks = nil | 1930 | wv.asyncCallbacks = nil | ||
| 1930 | - | wv.browsingContextDecision = port.HostDecision{} | 1931 | + | wv.browsingContextDecision = dto.HostDecision{} |
| 1931 | wv.hasBrowsingContextDecision = false | 1932 | wv.hasBrowsingContextDecision = false | ||
| 1932 | wv.nativePopupHostAbort = nil | 1933 | wv.nativePopupHostAbort = nil | ||
| 1933 | wv.mu.Unlock() | 1934 | wv.mu.Unlock() | ||
| @@ -1997,7 +1998,7 @@ func (wv *WebView) ResetForPoolReuse() { | |||||
| 1997 | wv.isLoading = false | 1998 | wv.isLoading = false | ||
| 1998 | wv.asyncCallbacks = nil | 1999 | wv.asyncCallbacks = nil | ||
| 1999 | wv.runJSErrorStats = make(map[string]runJSErrorStat) | 2000 | wv.runJSErrorStats = make(map[string]runJSErrorStat) | ||
| 2000 | - | wv.browsingContextDecision = port.HostDecision{} | 2001 | + | wv.browsingContextDecision = dto.HostDecision{} |
| 2001 | wv.hasBrowsingContextDecision = false | 2002 | wv.hasBrowsingContextDecision = false | ||
| 2002 | wv.nativePopupHostAbort = nil | 2003 | wv.nativePopupHostAbort = nil | ||
| 2003 | wv.lastProgressUpdate.Store(0) | 2004 | wv.lastProgressUpdate.Store(0) | ||
M internal/infrastructure/webkit/webview_reuse_state_test.go +5 -5
| @@ -3,7 +3,7 @@ package webkit | |||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | - | "github.com/bnema/dumber/internal/application/port" | |
| 6 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 7 | 7 | "github.com/stretchr/testify/assert" | |
| 8 | 8 | ) | |
| 9 | 9 | ||
| @@ -11,14 +11,14 @@ func TestResetForPoolReuse_ClearsBrowsingContextHostState(t *testing.T) { | |||
| 11 | 11 | wv := &WebView{} | |
| 12 | 12 | abortCalled := false | |
| 13 | 13 | ||
| 14 | - | wv.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreateNativeWin}) | |
| 14 | + | wv.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateNativeWin}) | |
| 15 | 15 | wv.SetNativePopupHostAbort(func() { abortCalled = true }) | |
| 16 | 16 | ||
| 17 | 17 | wv.ResetForPoolReuse() | |
| 18 | 18 | ||
| 19 | 19 | decision, hasDecision := wv.BrowsingContextHostDecision() | |
| 20 | 20 | assert.False(t, hasDecision) | |
| 21 | - | assert.Equal(t, port.HostDecision{}, decision) | |
| 21 | + | assert.Equal(t, dto.HostDecision{}, decision) | |
| 22 | 22 | ||
| 23 | 23 | wv.AbortNativePopupHost() | |
| 24 | 24 | assert.False(t, abortCalled) | |
| @@ -28,14 +28,14 @@ func TestDestroyWithPolicy_ClearsBrowsingContextHostState(t *testing.T) { | |||
| 28 | 28 | wv := &WebView{} | |
| 29 | 29 | abortCalled := false | |
| 30 | 30 | ||
| 31 | - | wv.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreatePane}) | |
| 31 | + | wv.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreatePane}) | |
| 32 | 32 | wv.SetNativePopupHostAbort(func() { abortCalled = true }) | |
| 33 | 33 | ||
| 34 | 34 | wv.DestroyWithPolicy(terminatePolicyNever) | |
| 35 | 35 | ||
| 36 | 36 | decision, hasDecision := wv.BrowsingContextHostDecision() | |
| 37 | 37 | assert.False(t, hasDecision) | |
| 38 | - | assert.Equal(t, port.HostDecision{}, decision) | |
| 38 | + | assert.Equal(t, dto.HostDecision{}, decision) | |
| 39 | 39 | ||
| 40 | 40 | wv.AbortNativePopupHost() | |
| 41 | 41 | assert.False(t, abortCalled) | |
| @@ -3,7 +3,7 @@ package webkit | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | - | "github.com/bnema/dumber/internal/application/port" | 6 | + | "github.com/bnema/dumber/internal/application/dto" |
| 7 | "github.com/stretchr/testify/assert" | 7 | "github.com/stretchr/testify/assert" | ||
| 8 | ) | 8 | ) | ||
| 9 | 9 | ||||
| @@ -11,14 +11,14 @@ func TestResetForPoolReuse_ClearsBrowsingContextHostState(t *testing.T) { | |||||
| 11 | wv := &WebView{} | 11 | wv := &WebView{} | ||
| 12 | abortCalled := false | 12 | abortCalled := false | ||
| 13 | 13 | ||||
| 14 | - | wv.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreateNativeWin}) | 14 | + | wv.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreateNativeWin}) |
| 15 | wv.SetNativePopupHostAbort(func() { abortCalled = true }) | 15 | wv.SetNativePopupHostAbort(func() { abortCalled = true }) | ||
| 16 | 16 | ||||
| 17 | wv.ResetForPoolReuse() | 17 | wv.ResetForPoolReuse() | ||
| 18 | 18 | ||||
| 19 | decision, hasDecision := wv.BrowsingContextHostDecision() | 19 | decision, hasDecision := wv.BrowsingContextHostDecision() | ||
| 20 | assert.False(t, hasDecision) | 20 | assert.False(t, hasDecision) | ||
| 21 | - | assert.Equal(t, port.HostDecision{}, decision) | 21 | + | assert.Equal(t, dto.HostDecision{}, decision) |
| 22 | 22 | ||||
| 23 | wv.AbortNativePopupHost() | 23 | wv.AbortNativePopupHost() | ||
| 24 | assert.False(t, abortCalled) | 24 | assert.False(t, abortCalled) | ||
| @@ -28,14 +28,14 @@ func TestDestroyWithPolicy_ClearsBrowsingContextHostState(t *testing.T) { | |||||
| 28 | wv := &WebView{} | 28 | wv := &WebView{} | ||
| 29 | abortCalled := false | 29 | abortCalled := false | ||
| 30 | 30 | ||||
| 31 | - | wv.SetBrowsingContextHostDecision(port.HostDecision{Kind: port.HostDecisionCreatePane}) | 31 | + | wv.SetBrowsingContextHostDecision(dto.HostDecision{Kind: dto.HostDecisionCreatePane}) |
| 32 | wv.SetNativePopupHostAbort(func() { abortCalled = true }) | 32 | wv.SetNativePopupHostAbort(func() { abortCalled = true }) | ||
| 33 | 33 | ||||
| 34 | wv.DestroyWithPolicy(terminatePolicyNever) | 34 | wv.DestroyWithPolicy(terminatePolicyNever) | ||
| 35 | 35 | ||||
| 36 | decision, hasDecision := wv.BrowsingContextHostDecision() | 36 | decision, hasDecision := wv.BrowsingContextHostDecision() | ||
| 37 | assert.False(t, hasDecision) | 37 | assert.False(t, hasDecision) | ||
| 38 | - | assert.Equal(t, port.HostDecision{}, decision) | 38 | + | assert.Equal(t, dto.HostDecision{}, decision) |
| 39 | 39 | ||||
| 40 | wv.AbortNativePopupHost() | 40 | wv.AbortNativePopupHost() | ||
| 41 | assert.False(t, abortCalled) | 41 | assert.False(t, abortCalled) | ||
M internal/ui/coordinator/content/auth_window_observer.go +18 -8
| @@ -2,6 +2,7 @@ package content | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "sync" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| 7 | 8 | "github.com/bnema/dumber/internal/application/port" | |
| @@ -11,6 +12,16 @@ import ( | |||
| 11 | 12 | ||
| 12 | 13 | const nativePopupOAuthCloseDelay = 500 * time.Millisecond | |
| 13 | 14 | ||
| 15 | + | var scheduleNativePopupOAuthClose = func(delay time.Duration, fn func()) { | |
| 16 | + | time.AfterFunc(delay, func() { | |
| 17 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 18 | + | fn() | |
| 19 | + | return false | |
| 20 | + | }) | |
| 21 | + | glib.IdleAdd(&cb, 0) | |
| 22 | + | }) | |
| 23 | + | } | |
| 24 | + | ||
| 14 | 25 | func (c *Coordinator) ObserveNativePopupAuth(ctx context.Context, input NativePopupInput) { | |
| 15 | 26 | if c == nil || input.PopupWebView == nil || !IsOAuthURL(input.TargetURI) { | |
| 16 | 27 | return | |
| @@ -27,19 +38,18 @@ func (c *Coordinator) ObserveNativePopupAuth(ctx context.Context, input NativePo | |||
| 27 | 38 | } | |
| 28 | 39 | c.trackOAuthPopup(popupID, input.ParentPaneID, input.ParentURIAtOpen) | |
| 29 | 40 | ||
| 41 | + | var requestCloseOnce sync.Once | |
| 30 | 42 | requestClose := func(reason string) { | |
| 31 | - | logging.FromContext(ctx).Info(). | |
| 32 | - | Uint64("popup_id", uint64(popupID)). | |
| 33 | - | Str("reason", reason). | |
| 34 | - | Msg("native popup oauth callback detected, closing popup") | |
| 35 | - | time.AfterFunc(nativePopupOAuthCloseDelay, func() { | |
| 36 | - | cb := glib.SourceFunc(func(_ uintptr) bool { | |
| 43 | + | requestCloseOnce.Do(func() { | |
| 44 | + | logging.FromContext(ctx).Info(). | |
| 45 | + | Uint64("popup_id", uint64(popupID)). | |
| 46 | + | Str("reason", reason). | |
| 47 | + | Msg("native popup oauth callback detected, closing popup") | |
| 48 | + | scheduleNativePopupOAuthClose(nativePopupOAuthCloseDelay, func() { | |
| 37 | 49 | if input.PopupWebView != nil && !input.PopupWebView.IsDestroyed() { | |
| 38 | 50 | oauthWV.Close() | |
| 39 | 51 | } | |
| 40 | - | return false | |
| 41 | 52 | }) | |
| 42 | - | glib.IdleAdd(&cb, 0) | |
| 43 | 53 | }) | |
| 44 | 54 | } | |
| 45 | 55 | ||
| @@ -2,6 +2,7 @@ package content | |||||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "context" | 4 | "context" | ||
| 5 | + | "sync" | |||
| 5 | "time" | 6 | "time" | ||
| 6 | 7 | ||||
| 7 | "github.com/bnema/dumber/internal/application/port" | 8 | "github.com/bnema/dumber/internal/application/port" | ||
| @@ -11,6 +12,16 @@ import ( | |||||
| 11 | 12 | ||||
| 12 | const nativePopupOAuthCloseDelay = 500 * time.Millisecond | 13 | const nativePopupOAuthCloseDelay = 500 * time.Millisecond | ||
| 13 | 14 | ||||
| 15 | + | var scheduleNativePopupOAuthClose = func(delay time.Duration, fn func()) { | |||
| 16 | + | time.AfterFunc(delay, func() { | |||
| 17 | + | cb := glib.SourceFunc(func(_ uintptr) bool { | |||
| 18 | + | fn() | |||
| 19 | + | return false | |||
| 20 | + | }) | |||
| 21 | + | glib.IdleAdd(&cb, 0) | |||
| 22 | + | }) | |||
| 23 | + | } | |||
| 24 | + | ||||
| 14 | func (c *Coordinator) ObserveNativePopupAuth(ctx context.Context, input NativePopupInput) { | 25 | func (c *Coordinator) ObserveNativePopupAuth(ctx context.Context, input NativePopupInput) { | ||
| 15 | if c == nil || input.PopupWebView == nil || !IsOAuthURL(input.TargetURI) { | 26 | if c == nil || input.PopupWebView == nil || !IsOAuthURL(input.TargetURI) { | ||
| 16 | return | 27 | return | ||
| @@ -27,19 +38,18 @@ func (c *Coordinator) ObserveNativePopupAuth(ctx context.Context, input NativePo | |||||
| 27 | } | 38 | } | ||
| 28 | c.trackOAuthPopup(popupID, input.ParentPaneID, input.ParentURIAtOpen) | 39 | c.trackOAuthPopup(popupID, input.ParentPaneID, input.ParentURIAtOpen) | ||
| 29 | 40 | ||||
| 41 | + | var requestCloseOnce sync.Once | |||
| 30 | requestClose := func(reason string) { | 42 | requestClose := func(reason string) { | ||
| 31 | - | logging.FromContext(ctx).Info(). | 43 | + | requestCloseOnce.Do(func() { |
| 32 | - | Uint64("popup_id", uint64(popupID)). | 44 | + | logging.FromContext(ctx).Info(). |
| 33 | - | Str("reason", reason). | 45 | + | Uint64("popup_id", uint64(popupID)). |
| 34 | - | Msg("native popup oauth callback detected, closing popup") | 46 | + | Str("reason", reason). |
| 35 | - | time.AfterFunc(nativePopupOAuthCloseDelay, func() { | 47 | + | Msg("native popup oauth callback detected, closing popup") |
| 36 | - | cb := glib.SourceFunc(func(_ uintptr) bool { | 48 | + | scheduleNativePopupOAuthClose(nativePopupOAuthCloseDelay, func() { |
| 37 | if input.PopupWebView != nil && !input.PopupWebView.IsDestroyed() { | 49 | if input.PopupWebView != nil && !input.PopupWebView.IsDestroyed() { | ||
| 38 | oauthWV.Close() | 50 | oauthWV.Close() | ||
| 39 | } | 51 | } | ||
| 40 | - | return false | |||
| 41 | }) | 52 | }) | ||
| 42 | - | glib.IdleAdd(&cb, 0) | |||
| 43 | }) | 53 | }) | ||
| 44 | } | 54 | } | ||
| 45 | 55 | ||||
M internal/ui/coordinator/content/browsing_context_policy.go +37 -36
| @@ -3,60 +3,61 @@ package content | |||
| 3 | 3 | import ( | |
| 4 | 4 | "strings" | |
| 5 | 5 | ||
| 6 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 6 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| 9 | 10 | type browsingContextPolicy struct{} | |
| 10 | 11 | ||
| 11 | - | func (browsingContextPolicy) Decide(req port.NewBrowsingContextRequest, namedContextExists bool) port.HostDecision { | |
| 12 | - | decision := port.HostDecision{ | |
| 12 | + | func (browsingContextPolicy) Decide(req dto.NewBrowsingContextRequest, namedContextExists bool) dto.HostDecision { | |
| 13 | + | decision := dto.HostDecision{ | |
| 13 | 14 | RequestContextDisposition: req.RequestContextDisposition, | |
| 14 | 15 | RequiresNativeOpener: req.RequiresNativeOpener, | |
| 15 | 16 | } | |
| 16 | 17 | if decision.RequestContextDisposition == "" { | |
| 17 | - | decision.RequestContextDisposition = port.RequestContextInheritParent | |
| 18 | + | decision.RequestContextDisposition = dto.RequestContextInheritParent | |
| 18 | 19 | } | |
| 19 | 20 | ||
| 20 | 21 | if strings.TrimSpace(req.TargetURI) == "" { | |
| 21 | - | decision.Kind = port.HostDecisionDeny | |
| 22 | + | decision.Kind = dto.HostDecisionDeny | |
| 22 | 23 | decision.Reason = "empty target URI" | |
| 23 | 24 | return decision | |
| 24 | 25 | } | |
| 25 | 26 | ||
| 26 | 27 | if req.AuthIntent { | |
| 27 | - | decision.Kind = port.HostDecisionCreateNativeWin | |
| 28 | + | decision.Kind = dto.HostDecisionCreateNativeWin | |
| 28 | 29 | decision.Reason = "auth intent requires native window" | |
| 29 | 30 | return decision | |
| 30 | 31 | } | |
| 31 | 32 | if req.RequiresNativeOpener { | |
| 32 | - | decision.Kind = port.HostDecisionCreateNativeWin | |
| 33 | + | decision.Kind = dto.HostDecisionCreateNativeWin | |
| 33 | 34 | decision.Reason = "native opener required" | |
| 34 | 35 | return decision | |
| 35 | 36 | } | |
| 36 | 37 | if isAmbiguousNativeBrowsingContext(req) { | |
| 37 | - | decision.Kind = port.HostDecisionCreateNativeWin | |
| 38 | + | decision.Kind = dto.HostDecisionCreateNativeWin | |
| 38 | 39 | decision.Reason = "ambiguous opener-coupled request prefers native window" | |
| 39 | 40 | return decision | |
| 40 | 41 | } | |
| 41 | 42 | ||
| 42 | 43 | name := reusableBrowsingContextName(req.TargetFrameName) | |
| 43 | 44 | if namedContextExists && name != "" { | |
| 44 | - | decision.Kind = port.HostDecisionReuseNamedPane | |
| 45 | + | decision.Kind = dto.HostDecisionReuseNamedPane | |
| 45 | 46 | decision.ReuseContextName = name | |
| 46 | 47 | decision.BrowsingContextName = name | |
| 47 | 48 | decision.Reason = "reuse named browsing context in current window" | |
| 48 | 49 | return decision | |
| 49 | 50 | } | |
| 50 | 51 | ||
| 51 | - | decision.Kind = port.HostDecisionCreatePane | |
| 52 | + | decision.Kind = dto.HostDecisionCreatePane | |
| 52 | 53 | decision.BrowsingContextName = name | |
| 53 | 54 | decision.Reason = "pane-hosted browsing context" | |
| 54 | 55 | return decision | |
| 55 | 56 | } | |
| 56 | 57 | ||
| 57 | - | func buildPopupBrowsingContextRequest(req port.PopupRequest) port.NewBrowsingContextRequest { | |
| 58 | - | return port.NewBrowsingContextRequest{ | |
| 59 | - | ParentWebViewID: req.ParentViewID, | |
| 58 | + | func buildPopupBrowsingContextRequest(req port.PopupRequest) dto.NewBrowsingContextRequest { | |
| 59 | + | return dto.NewBrowsingContextRequest{ | |
| 60 | + | ParentWebViewID: uint64(req.ParentViewID), | |
| 60 | 61 | SourceBrowserID: req.SourceBrowserID, | |
| 61 | 62 | SourceFrameID: req.SourceFrameID, | |
| 62 | 63 | SourceFrameURL: req.SourceFrameURL, | |
| @@ -69,61 +70,61 @@ func buildPopupBrowsingContextRequest(req port.PopupRequest) port.NewBrowsingCon | |||
| 69 | 70 | TriggerKind: inferPopupTriggerKind(req), | |
| 70 | 71 | AuthIntent: IsOAuthURL(req.TargetURI), | |
| 71 | 72 | RequiresNativeOpener: false, | |
| 72 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 73 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 73 | 74 | } | |
| 74 | 75 | } | |
| 75 | 76 | ||
| 76 | - | func buildLinkBrowsingContextRequest(parentWebViewID port.WebViewID, uri string) port.NewBrowsingContextRequest { | |
| 77 | - | return port.NewBrowsingContextRequest{ | |
| 78 | - | ParentWebViewID: parentWebViewID, | |
| 77 | + | func buildLinkBrowsingContextRequest(parentWebViewID port.WebViewID, uri string) dto.NewBrowsingContextRequest { | |
| 78 | + | return dto.NewBrowsingContextRequest{ | |
| 79 | + | ParentWebViewID: uint64(parentWebViewID), | |
| 79 | 80 | TargetURI: uri, | |
| 80 | 81 | TargetFrameName: "_blank", | |
| 81 | - | TargetDisposition: port.WindowDispositionNewTab, | |
| 82 | + | TargetDisposition: dto.WindowDispositionNewTab, | |
| 82 | 83 | IsUserGesture: true, | |
| 83 | 84 | NoJavaScriptAccess: true, | |
| 84 | - | TriggerKind: port.TriggerLinkNewPage, | |
| 85 | + | TriggerKind: dto.TriggerLinkNewPage, | |
| 85 | 86 | AuthIntent: IsOAuthURL(uri), | |
| 86 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 87 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 87 | 88 | } | |
| 88 | 89 | } | |
| 89 | 90 | ||
| 90 | - | func inferPopupWindowDisposition(req port.PopupRequest) port.WindowDisposition { | |
| 91 | + | func inferPopupWindowDisposition(req port.PopupRequest) dto.WindowDisposition { | |
| 91 | 92 | if req.TargetDisposition != "" { | |
| 92 | 93 | return req.TargetDisposition | |
| 93 | 94 | } | |
| 94 | 95 | if strings.EqualFold(strings.TrimSpace(req.FrameName), "_blank") { | |
| 95 | - | return port.WindowDispositionNewTab | |
| 96 | + | return dto.WindowDispositionNewTab | |
| 96 | 97 | } | |
| 97 | - | return port.WindowDispositionNewPopup | |
| 98 | + | return dto.WindowDispositionNewPopup | |
| 98 | 99 | } | |
| 99 | 100 | ||
| 100 | - | func inferPopupTriggerKind(req port.PopupRequest) port.TriggerKind { | |
| 101 | + | func inferPopupTriggerKind(req port.PopupRequest) dto.TriggerKind { | |
| 101 | 102 | switch inferPopupWindowDisposition(req) { | |
| 102 | - | case port.WindowDispositionCurrentTab: | |
| 103 | + | case dto.WindowDispositionCurrentTab: | |
| 103 | 104 | if reusableBrowsingContextName(req.FrameName) != "" { | |
| 104 | - | return port.TriggerNamedTargetNavigation | |
| 105 | + | return dto.TriggerNamedTargetNavigation | |
| 105 | 106 | } | |
| 106 | - | return port.TriggerLinkNewPage | |
| 107 | - | case port.WindowDispositionNewTab: | |
| 107 | + | return dto.TriggerLinkNewPage | |
| 108 | + | case dto.WindowDispositionNewTab: | |
| 108 | 109 | if reusableBrowsingContextName(req.FrameName) != "" { | |
| 109 | - | return port.TriggerNamedTargetNavigation | |
| 110 | + | return dto.TriggerNamedTargetNavigation | |
| 110 | 111 | } | |
| 111 | - | return port.TriggerLinkNewPage | |
| 112 | - | case port.WindowDispositionNewPopup, port.WindowDispositionNewWindow: | |
| 112 | + | return dto.TriggerLinkNewPage | |
| 113 | + | case dto.WindowDispositionNewPopup, dto.WindowDispositionNewWindow: | |
| 113 | 114 | if reusableBrowsingContextName(req.FrameName) != "" { | |
| 114 | - | return port.TriggerNamedTargetNavigation | |
| 115 | + | return dto.TriggerNamedTargetNavigation | |
| 115 | 116 | } | |
| 116 | - | return port.TriggerScriptWindowOpen | |
| 117 | + | return dto.TriggerScriptWindowOpen | |
| 117 | 118 | default: | |
| 118 | 119 | if reusableBrowsingContextName(req.FrameName) != "" { | |
| 119 | - | return port.TriggerNamedTargetNavigation | |
| 120 | + | return dto.TriggerNamedTargetNavigation | |
| 120 | 121 | } | |
| 121 | - | return port.TriggerUnknown | |
| 122 | + | return dto.TriggerUnknown | |
| 122 | 123 | } | |
| 123 | 124 | } | |
| 124 | 125 | ||
| 125 | - | func isAmbiguousNativeBrowsingContext(req port.NewBrowsingContextRequest) bool { | |
| 126 | - | return req.TriggerKind == port.TriggerUnknown && !req.NoJavaScriptAccess | |
| 126 | + | func isAmbiguousNativeBrowsingContext(req dto.NewBrowsingContextRequest) bool { | |
| 127 | + | return req.TriggerKind == dto.TriggerUnknown && !req.NoJavaScriptAccess | |
| 127 | 128 | } | |
| 128 | 129 | ||
| 129 | 130 | func reusableBrowsingContextName(frameName string) string { | |
| @@ -3,60 +3,61 @@ package content | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "strings" | 4 | "strings" | ||
| 5 | 5 | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 6 | "github.com/bnema/dumber/internal/application/port" | 7 | "github.com/bnema/dumber/internal/application/port" | ||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| 9 | type browsingContextPolicy struct{} | 10 | type browsingContextPolicy struct{} | ||
| 10 | 11 | ||||
| 11 | - | func (browsingContextPolicy) Decide(req port.NewBrowsingContextRequest, namedContextExists bool) port.HostDecision { | 12 | + | func (browsingContextPolicy) Decide(req dto.NewBrowsingContextRequest, namedContextExists bool) dto.HostDecision { |
| 12 | - | decision := port.HostDecision{ | 13 | + | decision := dto.HostDecision{ |
| 13 | RequestContextDisposition: req.RequestContextDisposition, | 14 | RequestContextDisposition: req.RequestContextDisposition, | ||
| 14 | RequiresNativeOpener: req.RequiresNativeOpener, | 15 | RequiresNativeOpener: req.RequiresNativeOpener, | ||
| 15 | } | 16 | } | ||
| 16 | if decision.RequestContextDisposition == "" { | 17 | if decision.RequestContextDisposition == "" { | ||
| 17 | - | decision.RequestContextDisposition = port.RequestContextInheritParent | 18 | + | decision.RequestContextDisposition = dto.RequestContextInheritParent |
| 18 | } | 19 | } | ||
| 19 | 20 | ||||
| 20 | if strings.TrimSpace(req.TargetURI) == "" { | 21 | if strings.TrimSpace(req.TargetURI) == "" { | ||
| 21 | - | decision.Kind = port.HostDecisionDeny | 22 | + | decision.Kind = dto.HostDecisionDeny |
| 22 | decision.Reason = "empty target URI" | 23 | decision.Reason = "empty target URI" | ||
| 23 | return decision | 24 | return decision | ||
| 24 | } | 25 | } | ||
| 25 | 26 | ||||
| 26 | if req.AuthIntent { | 27 | if req.AuthIntent { | ||
| 27 | - | decision.Kind = port.HostDecisionCreateNativeWin | 28 | + | decision.Kind = dto.HostDecisionCreateNativeWin |
| 28 | decision.Reason = "auth intent requires native window" | 29 | decision.Reason = "auth intent requires native window" | ||
| 29 | return decision | 30 | return decision | ||
| 30 | } | 31 | } | ||
| 31 | if req.RequiresNativeOpener { | 32 | if req.RequiresNativeOpener { | ||
| 32 | - | decision.Kind = port.HostDecisionCreateNativeWin | 33 | + | decision.Kind = dto.HostDecisionCreateNativeWin |
| 33 | decision.Reason = "native opener required" | 34 | decision.Reason = "native opener required" | ||
| 34 | return decision | 35 | return decision | ||
| 35 | } | 36 | } | ||
| 36 | if isAmbiguousNativeBrowsingContext(req) { | 37 | if isAmbiguousNativeBrowsingContext(req) { | ||
| 37 | - | decision.Kind = port.HostDecisionCreateNativeWin | 38 | + | decision.Kind = dto.HostDecisionCreateNativeWin |
| 38 | decision.Reason = "ambiguous opener-coupled request prefers native window" | 39 | decision.Reason = "ambiguous opener-coupled request prefers native window" | ||
| 39 | return decision | 40 | return decision | ||
| 40 | } | 41 | } | ||
| 41 | 42 | ||||
| 42 | name := reusableBrowsingContextName(req.TargetFrameName) | 43 | name := reusableBrowsingContextName(req.TargetFrameName) | ||
| 43 | if namedContextExists && name != "" { | 44 | if namedContextExists && name != "" { | ||
| 44 | - | decision.Kind = port.HostDecisionReuseNamedPane | 45 | + | decision.Kind = dto.HostDecisionReuseNamedPane |
| 45 | decision.ReuseContextName = name | 46 | decision.ReuseContextName = name | ||
| 46 | decision.BrowsingContextName = name | 47 | decision.BrowsingContextName = name | ||
| 47 | decision.Reason = "reuse named browsing context in current window" | 48 | decision.Reason = "reuse named browsing context in current window" | ||
| 48 | return decision | 49 | return decision | ||
| 49 | } | 50 | } | ||
| 50 | 51 | ||||
| 51 | - | decision.Kind = port.HostDecisionCreatePane | 52 | + | decision.Kind = dto.HostDecisionCreatePane |
| 52 | decision.BrowsingContextName = name | 53 | decision.BrowsingContextName = name | ||
| 53 | decision.Reason = "pane-hosted browsing context" | 54 | decision.Reason = "pane-hosted browsing context" | ||
| 54 | return decision | 55 | return decision | ||
| 55 | } | 56 | } | ||
| 56 | 57 | ||||
| 57 | - | func buildPopupBrowsingContextRequest(req port.PopupRequest) port.NewBrowsingContextRequest { | 58 | + | func buildPopupBrowsingContextRequest(req port.PopupRequest) dto.NewBrowsingContextRequest { |
| 58 | - | return port.NewBrowsingContextRequest{ | 59 | + | return dto.NewBrowsingContextRequest{ |
| 59 | - | ParentWebViewID: req.ParentViewID, | 60 | + | ParentWebViewID: uint64(req.ParentViewID), |
| 60 | SourceBrowserID: req.SourceBrowserID, | 61 | SourceBrowserID: req.SourceBrowserID, | ||
| 61 | SourceFrameID: req.SourceFrameID, | 62 | SourceFrameID: req.SourceFrameID, | ||
| 62 | SourceFrameURL: req.SourceFrameURL, | 63 | SourceFrameURL: req.SourceFrameURL, | ||
| @@ -69,61 +70,61 @@ func buildPopupBrowsingContextRequest(req port.PopupRequest) port.NewBrowsingCon | |||||
| 69 | TriggerKind: inferPopupTriggerKind(req), | 70 | TriggerKind: inferPopupTriggerKind(req), | ||
| 70 | AuthIntent: IsOAuthURL(req.TargetURI), | 71 | AuthIntent: IsOAuthURL(req.TargetURI), | ||
| 71 | RequiresNativeOpener: false, | 72 | RequiresNativeOpener: false, | ||
| 72 | - | RequestContextDisposition: port.RequestContextInheritParent, | 73 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 73 | } | 74 | } | ||
| 74 | } | 75 | } | ||
| 75 | 76 | ||||
| 76 | - | func buildLinkBrowsingContextRequest(parentWebViewID port.WebViewID, uri string) port.NewBrowsingContextRequest { | 77 | + | func buildLinkBrowsingContextRequest(parentWebViewID port.WebViewID, uri string) dto.NewBrowsingContextRequest { |
| 77 | - | return port.NewBrowsingContextRequest{ | 78 | + | return dto.NewBrowsingContextRequest{ |
| 78 | - | ParentWebViewID: parentWebViewID, | 79 | + | ParentWebViewID: uint64(parentWebViewID), |
| 79 | TargetURI: uri, | 80 | TargetURI: uri, | ||
| 80 | TargetFrameName: "_blank", | 81 | TargetFrameName: "_blank", | ||
| 81 | - | TargetDisposition: port.WindowDispositionNewTab, | 82 | + | TargetDisposition: dto.WindowDispositionNewTab, |
| 82 | IsUserGesture: true, | 83 | IsUserGesture: true, | ||
| 83 | NoJavaScriptAccess: true, | 84 | NoJavaScriptAccess: true, | ||
| 84 | - | TriggerKind: port.TriggerLinkNewPage, | 85 | + | TriggerKind: dto.TriggerLinkNewPage, |
| 85 | AuthIntent: IsOAuthURL(uri), | 86 | AuthIntent: IsOAuthURL(uri), | ||
| 86 | - | RequestContextDisposition: port.RequestContextInheritParent, | 87 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 87 | } | 88 | } | ||
| 88 | } | 89 | } | ||
| 89 | 90 | ||||
| 90 | - | func inferPopupWindowDisposition(req port.PopupRequest) port.WindowDisposition { | 91 | + | func inferPopupWindowDisposition(req port.PopupRequest) dto.WindowDisposition { |
| 91 | if req.TargetDisposition != "" { | 92 | if req.TargetDisposition != "" { | ||
| 92 | return req.TargetDisposition | 93 | return req.TargetDisposition | ||
| 93 | } | 94 | } | ||
| 94 | if strings.EqualFold(strings.TrimSpace(req.FrameName), "_blank") { | 95 | if strings.EqualFold(strings.TrimSpace(req.FrameName), "_blank") { | ||
| 95 | - | return port.WindowDispositionNewTab | 96 | + | return dto.WindowDispositionNewTab |
| 96 | } | 97 | } | ||
| 97 | - | return port.WindowDispositionNewPopup | 98 | + | return dto.WindowDispositionNewPopup |
| 98 | } | 99 | } | ||
| 99 | 100 | ||||
| 100 | - | func inferPopupTriggerKind(req port.PopupRequest) port.TriggerKind { | 101 | + | func inferPopupTriggerKind(req port.PopupRequest) dto.TriggerKind { |
| 101 | switch inferPopupWindowDisposition(req) { | 102 | switch inferPopupWindowDisposition(req) { | ||
| 102 | - | case port.WindowDispositionCurrentTab: | 103 | + | case dto.WindowDispositionCurrentTab: |
| 103 | if reusableBrowsingContextName(req.FrameName) != "" { | 104 | if reusableBrowsingContextName(req.FrameName) != "" { | ||
| 104 | - | return port.TriggerNamedTargetNavigation | 105 | + | return dto.TriggerNamedTargetNavigation |
| 105 | } | 106 | } | ||
| 106 | - | return port.TriggerLinkNewPage | 107 | + | return dto.TriggerLinkNewPage |
| 107 | - | case port.WindowDispositionNewTab: | 108 | + | case dto.WindowDispositionNewTab: |
| 108 | if reusableBrowsingContextName(req.FrameName) != "" { | 109 | if reusableBrowsingContextName(req.FrameName) != "" { | ||
| 109 | - | return port.TriggerNamedTargetNavigation | 110 | + | return dto.TriggerNamedTargetNavigation |
| 110 | } | 111 | } | ||
| 111 | - | return port.TriggerLinkNewPage | 112 | + | return dto.TriggerLinkNewPage |
| 112 | - | case port.WindowDispositionNewPopup, port.WindowDispositionNewWindow: | 113 | + | case dto.WindowDispositionNewPopup, dto.WindowDispositionNewWindow: |
| 113 | if reusableBrowsingContextName(req.FrameName) != "" { | 114 | if reusableBrowsingContextName(req.FrameName) != "" { | ||
| 114 | - | return port.TriggerNamedTargetNavigation | 115 | + | return dto.TriggerNamedTargetNavigation |
| 115 | } | 116 | } | ||
| 116 | - | return port.TriggerScriptWindowOpen | 117 | + | return dto.TriggerScriptWindowOpen |
| 117 | default: | 118 | default: | ||
| 118 | if reusableBrowsingContextName(req.FrameName) != "" { | 119 | if reusableBrowsingContextName(req.FrameName) != "" { | ||
| 119 | - | return port.TriggerNamedTargetNavigation | 120 | + | return dto.TriggerNamedTargetNavigation |
| 120 | } | 121 | } | ||
| 121 | - | return port.TriggerUnknown | 122 | + | return dto.TriggerUnknown |
| 122 | } | 123 | } | ||
| 123 | } | 124 | } | ||
| 124 | 125 | ||||
| 125 | - | func isAmbiguousNativeBrowsingContext(req port.NewBrowsingContextRequest) bool { | 126 | + | func isAmbiguousNativeBrowsingContext(req dto.NewBrowsingContextRequest) bool { |
| 126 | - | return req.TriggerKind == port.TriggerUnknown && !req.NoJavaScriptAccess | 127 | + | return req.TriggerKind == dto.TriggerUnknown && !req.NoJavaScriptAccess |
| 127 | } | 128 | } | ||
| 128 | 129 | ||||
| 129 | func reusableBrowsingContextName(frameName string) string { | 130 | func reusableBrowsingContextName(frameName string) string { | ||
M internal/ui/coordinator/content/browsing_context_policy_test.go +43 -42
| @@ -3,6 +3,7 @@ package content | |||
| 3 | 3 | import ( | |
| 4 | 4 | "testing" | |
| 5 | 5 | ||
| 6 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 6 | 7 | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 8 | "github.com/stretchr/testify/assert" | |
| 8 | 9 | ) | |
| @@ -13,76 +14,76 @@ func TestBrowsingContextPolicyDecide(t *testing.T) { | |||
| 13 | 14 | policy := browsingContextPolicy{} | |
| 14 | 15 | tests := []struct { | |
| 15 | 16 | name string | |
| 16 | - | request port.NewBrowsingContextRequest | |
| 17 | + | request dto.NewBrowsingContextRequest | |
| 17 | 18 | namedContextExist bool | |
| 18 | - | wantKind port.HostDecisionKind | |
| 19 | + | wantKind dto.HostDecisionKind | |
| 19 | 20 | wantName string | |
| 20 | 21 | }{ | |
| 21 | 22 | { | |
| 22 | 23 | name: "blank target stays pane-hosted", | |
| 23 | - | request: port.NewBrowsingContextRequest{ | |
| 24 | + | request: dto.NewBrowsingContextRequest{ | |
| 24 | 25 | TargetURI: "https://example.com/docs", | |
| 25 | 26 | TargetFrameName: "_blank", | |
| 26 | - | TriggerKind: port.TriggerLinkNewPage, | |
| 27 | - | TargetDisposition: port.WindowDispositionNewTab, | |
| 28 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 27 | + | TriggerKind: dto.TriggerLinkNewPage, | |
| 28 | + | TargetDisposition: dto.WindowDispositionNewTab, | |
| 29 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 29 | 30 | }, | |
| 30 | - | wantKind: port.HostDecisionCreatePane, | |
| 31 | + | wantKind: dto.HostDecisionCreatePane, | |
| 31 | 32 | }, | |
| 32 | 33 | { | |
| 33 | 34 | name: "named target reuses existing pane in same window", | |
| 34 | - | request: port.NewBrowsingContextRequest{ | |
| 35 | + | request: dto.NewBrowsingContextRequest{ | |
| 35 | 36 | TargetURI: "https://example.com/second", | |
| 36 | 37 | TargetFrameName: "shared-pane", | |
| 37 | - | TriggerKind: port.TriggerNamedTargetNavigation, | |
| 38 | - | TargetDisposition: port.WindowDispositionNewPopup, | |
| 39 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 38 | + | TriggerKind: dto.TriggerNamedTargetNavigation, | |
| 39 | + | TargetDisposition: dto.WindowDispositionNewPopup, | |
| 40 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 40 | 41 | }, | |
| 41 | 42 | namedContextExist: true, | |
| 42 | - | wantKind: port.HostDecisionReuseNamedPane, | |
| 43 | + | wantKind: dto.HostDecisionReuseNamedPane, | |
| 43 | 44 | wantName: "shared-pane", | |
| 44 | 45 | }, | |
| 45 | 46 | { | |
| 46 | 47 | name: "ordinary window.open stays pane-hosted", | |
| 47 | - | request: port.NewBrowsingContextRequest{ | |
| 48 | + | request: dto.NewBrowsingContextRequest{ | |
| 48 | 49 | TargetURI: "https://example.com/help", | |
| 49 | - | TriggerKind: port.TriggerScriptWindowOpen, | |
| 50 | - | TargetDisposition: port.WindowDispositionNewPopup, | |
| 50 | + | TriggerKind: dto.TriggerScriptWindowOpen, | |
| 51 | + | TargetDisposition: dto.WindowDispositionNewPopup, | |
| 51 | 52 | NoJavaScriptAccess: false, | |
| 52 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 53 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 53 | 54 | }, | |
| 54 | - | wantKind: port.HostDecisionCreatePane, | |
| 55 | + | wantKind: dto.HostDecisionCreatePane, | |
| 55 | 56 | }, | |
| 56 | 57 | { | |
| 57 | 58 | name: "auth intent forces native window", | |
| 58 | - | request: port.NewBrowsingContextRequest{ | |
| 59 | + | request: dto.NewBrowsingContextRequest{ | |
| 59 | 60 | TargetURI: "https://accounts.google.com/o/oauth2/v2/auth", | |
| 60 | - | TriggerKind: port.TriggerScriptWindowOpen, | |
| 61 | - | TargetDisposition: port.WindowDispositionNewPopup, | |
| 61 | + | TriggerKind: dto.TriggerScriptWindowOpen, | |
| 62 | + | TargetDisposition: dto.WindowDispositionNewPopup, | |
| 62 | 63 | AuthIntent: true, | |
| 63 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 64 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 64 | 65 | }, | |
| 65 | - | wantKind: port.HostDecisionCreateNativeWin, | |
| 66 | + | wantKind: dto.HostDecisionCreateNativeWin, | |
| 66 | 67 | }, | |
| 67 | 68 | { | |
| 68 | 69 | name: "ambiguous opener-coupled request prefers native", | |
| 69 | - | request: port.NewBrowsingContextRequest{ | |
| 70 | + | request: dto.NewBrowsingContextRequest{ | |
| 70 | 71 | TargetURI: "https://example.com/popup", | |
| 71 | - | TriggerKind: port.TriggerUnknown, | |
| 72 | - | TargetDisposition: port.WindowDispositionNewPopup, | |
| 72 | + | TriggerKind: dto.TriggerUnknown, | |
| 73 | + | TargetDisposition: dto.WindowDispositionNewPopup, | |
| 73 | 74 | NoJavaScriptAccess: false, | |
| 74 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 75 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 75 | 76 | }, | |
| 76 | - | wantKind: port.HostDecisionCreateNativeWin, | |
| 77 | + | wantKind: dto.HostDecisionCreateNativeWin, | |
| 77 | 78 | }, | |
| 78 | 79 | { | |
| 79 | 80 | name: "empty target is denied", | |
| 80 | - | request: port.NewBrowsingContextRequest{ | |
| 81 | + | request: dto.NewBrowsingContextRequest{ | |
| 81 | 82 | TargetURI: "", | |
| 82 | - | TriggerKind: port.TriggerLinkNewPage, | |
| 83 | - | RequestContextDisposition: port.RequestContextInheritParent, | |
| 83 | + | TriggerKind: dto.TriggerLinkNewPage, | |
| 84 | + | RequestContextDisposition: dto.RequestContextInheritParent, | |
| 84 | 85 | }, | |
| 85 | - | wantKind: port.HostDecisionDeny, | |
| 86 | + | wantKind: dto.HostDecisionDeny, | |
| 86 | 87 | }, | |
| 87 | 88 | } | |
| 88 | 89 | ||
| @@ -92,7 +93,7 @@ func TestBrowsingContextPolicyDecide(t *testing.T) { | |||
| 92 | 93 | got := policy.Decide(tt.request, tt.namedContextExist) | |
| 93 | 94 | assert.Equal(t, tt.wantKind, got.Kind) | |
| 94 | 95 | assert.Equal(t, tt.wantName, got.BrowsingContextName) | |
| 95 | - | assert.Equal(t, port.RequestContextInheritParent, got.RequestContextDisposition) | |
| 96 | + | assert.Equal(t, dto.RequestContextInheritParent, got.RequestContextDisposition) | |
| 96 | 97 | }) | |
| 97 | 98 | } | |
| 98 | 99 | } | |
| @@ -103,10 +104,10 @@ func TestInferPopupTriggerKind_DoesNotTreatNoJavaScriptAccessFalseAsNativeSignal | |||
| 103 | 104 | req := port.PopupRequest{ | |
| 104 | 105 | TargetURI: "https://example.com/window-open", | |
| 105 | 106 | NoJavaScriptAccess: false, | |
| 106 | - | TargetDisposition: port.WindowDispositionNewPopup, | |
| 107 | + | TargetDisposition: dto.WindowDispositionNewPopup, | |
| 107 | 108 | } | |
| 108 | 109 | ||
| 109 | - | assert.Equal(t, port.TriggerScriptWindowOpen, inferPopupTriggerKind(req)) | |
| 110 | + | assert.Equal(t, dto.TriggerScriptWindowOpen, inferPopupTriggerKind(req)) | |
| 110 | 111 | } | |
| 111 | 112 | ||
| 112 | 113 | func TestInferPopupTriggerKind_ClassifiesCurrentTabAsNavigation(t *testing.T) { | |
| @@ -115,24 +116,24 @@ func TestInferPopupTriggerKind_ClassifiesCurrentTabAsNavigation(t *testing.T) { | |||
| 115 | 116 | tests := []struct { | |
| 116 | 117 | name string | |
| 117 | 118 | req port.PopupRequest | |
| 118 | - | want port.TriggerKind | |
| 119 | + | want dto.TriggerKind | |
| 119 | 120 | }{ | |
| 120 | 121 | { | |
| 121 | 122 | name: "unnamed current-tab request stays a navigation", | |
| 122 | 123 | req: port.PopupRequest{ | |
| 123 | 124 | TargetURI: "https://example.com/docs", | |
| 124 | - | TargetDisposition: port.WindowDispositionCurrentTab, | |
| 125 | + | TargetDisposition: dto.WindowDispositionCurrentTab, | |
| 125 | 126 | }, | |
| 126 | - | want: port.TriggerLinkNewPage, | |
| 127 | + | want: dto.TriggerLinkNewPage, | |
| 127 | 128 | }, | |
| 128 | 129 | { | |
| 129 | 130 | name: "named current-tab request is treated as named target navigation", | |
| 130 | 131 | req: port.PopupRequest{ | |
| 131 | 132 | TargetURI: "https://example.com/docs", | |
| 132 | 133 | FrameName: "shared-pane", | |
| 133 | - | TargetDisposition: port.WindowDispositionCurrentTab, | |
| 134 | + | TargetDisposition: dto.WindowDispositionCurrentTab, | |
| 134 | 135 | }, | |
| 135 | - | want: port.TriggerNamedTargetNavigation, | |
| 136 | + | want: dto.TriggerNamedTargetNavigation, | |
| 136 | 137 | }, | |
| 137 | 138 | } | |
| 138 | 139 | ||
| @@ -150,17 +151,17 @@ func TestInferPopupWindowDisposition_NormalizesBlankTarget(t *testing.T) { | |||
| 150 | 151 | tests := []struct { | |
| 151 | 152 | name string | |
| 152 | 153 | req port.PopupRequest | |
| 153 | - | want port.WindowDisposition | |
| 154 | + | want dto.WindowDisposition | |
| 154 | 155 | }{ | |
| 155 | 156 | { | |
| 156 | 157 | name: "mixed-case blank target is treated as new tab", | |
| 157 | 158 | req: port.PopupRequest{FrameName: " _BlAnK "}, | |
| 158 | - | want: port.WindowDispositionNewTab, | |
| 159 | + | want: dto.WindowDispositionNewTab, | |
| 159 | 160 | }, | |
| 160 | 161 | { | |
| 161 | 162 | name: "named target still defaults to popup", | |
| 162 | 163 | req: port.PopupRequest{FrameName: "shared-pane"}, | |
| 163 | - | want: port.WindowDispositionNewPopup, | |
| 164 | + | want: dto.WindowDispositionNewPopup, | |
| 164 | 165 | }, | |
| 165 | 166 | } | |
| 166 | 167 | ||
| @@ -3,6 +3,7 @@ package content | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "testing" | 4 | "testing" | ||
| 5 | 5 | ||||
| 6 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 6 | "github.com/bnema/dumber/internal/application/port" | 7 | "github.com/bnema/dumber/internal/application/port" | ||
| 7 | "github.com/stretchr/testify/assert" | 8 | "github.com/stretchr/testify/assert" | ||
| 8 | ) | 9 | ) | ||
| @@ -13,76 +14,76 @@ func TestBrowsingContextPolicyDecide(t *testing.T) { | |||||
| 13 | policy := browsingContextPolicy{} | 14 | policy := browsingContextPolicy{} | ||
| 14 | tests := []struct { | 15 | tests := []struct { | ||
| 15 | name string | 16 | name string | ||
| 16 | - | request port.NewBrowsingContextRequest | 17 | + | request dto.NewBrowsingContextRequest |
| 17 | namedContextExist bool | 18 | namedContextExist bool | ||
| 18 | - | wantKind port.HostDecisionKind | 19 | + | wantKind dto.HostDecisionKind |
| 19 | wantName string | 20 | wantName string | ||
| 20 | }{ | 21 | }{ | ||
| 21 | { | 22 | { | ||
| 22 | name: "blank target stays pane-hosted", | 23 | name: "blank target stays pane-hosted", | ||
| 23 | - | request: port.NewBrowsingContextRequest{ | 24 | + | request: dto.NewBrowsingContextRequest{ |
| 24 | TargetURI: "https://example.com/docs", | 25 | TargetURI: "https://example.com/docs", | ||
| 25 | TargetFrameName: "_blank", | 26 | TargetFrameName: "_blank", | ||
| 26 | - | TriggerKind: port.TriggerLinkNewPage, | 27 | + | TriggerKind: dto.TriggerLinkNewPage, |
| 27 | - | TargetDisposition: port.WindowDispositionNewTab, | 28 | + | TargetDisposition: dto.WindowDispositionNewTab, |
| 28 | - | RequestContextDisposition: port.RequestContextInheritParent, | 29 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 29 | }, | 30 | }, | ||
| 30 | - | wantKind: port.HostDecisionCreatePane, | 31 | + | wantKind: dto.HostDecisionCreatePane, |
| 31 | }, | 32 | }, | ||
| 32 | { | 33 | { | ||
| 33 | name: "named target reuses existing pane in same window", | 34 | name: "named target reuses existing pane in same window", | ||
| 34 | - | request: port.NewBrowsingContextRequest{ | 35 | + | request: dto.NewBrowsingContextRequest{ |
| 35 | TargetURI: "https://example.com/second", | 36 | TargetURI: "https://example.com/second", | ||
| 36 | TargetFrameName: "shared-pane", | 37 | TargetFrameName: "shared-pane", | ||
| 37 | - | TriggerKind: port.TriggerNamedTargetNavigation, | 38 | + | TriggerKind: dto.TriggerNamedTargetNavigation, |
| 38 | - | TargetDisposition: port.WindowDispositionNewPopup, | 39 | + | TargetDisposition: dto.WindowDispositionNewPopup, |
| 39 | - | RequestContextDisposition: port.RequestContextInheritParent, | 40 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 40 | }, | 41 | }, | ||
| 41 | namedContextExist: true, | 42 | namedContextExist: true, | ||
| 42 | - | wantKind: port.HostDecisionReuseNamedPane, | 43 | + | wantKind: dto.HostDecisionReuseNamedPane, |
| 43 | wantName: "shared-pane", | 44 | wantName: "shared-pane", | ||
| 44 | }, | 45 | }, | ||
| 45 | { | 46 | { | ||
| 46 | name: "ordinary window.open stays pane-hosted", | 47 | name: "ordinary window.open stays pane-hosted", | ||
| 47 | - | request: port.NewBrowsingContextRequest{ | 48 | + | request: dto.NewBrowsingContextRequest{ |
| 48 | TargetURI: "https://example.com/help", | 49 | TargetURI: "https://example.com/help", | ||
| 49 | - | TriggerKind: port.TriggerScriptWindowOpen, | 50 | + | TriggerKind: dto.TriggerScriptWindowOpen, |
| 50 | - | TargetDisposition: port.WindowDispositionNewPopup, | 51 | + | TargetDisposition: dto.WindowDispositionNewPopup, |
| 51 | NoJavaScriptAccess: false, | 52 | NoJavaScriptAccess: false, | ||
| 52 | - | RequestContextDisposition: port.RequestContextInheritParent, | 53 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 53 | }, | 54 | }, | ||
| 54 | - | wantKind: port.HostDecisionCreatePane, | 55 | + | wantKind: dto.HostDecisionCreatePane, |
| 55 | }, | 56 | }, | ||
| 56 | { | 57 | { | ||
| 57 | name: "auth intent forces native window", | 58 | name: "auth intent forces native window", | ||
| 58 | - | request: port.NewBrowsingContextRequest{ | 59 | + | request: dto.NewBrowsingContextRequest{ |
| 59 | TargetURI: "https://accounts.google.com/o/oauth2/v2/auth", | 60 | TargetURI: "https://accounts.google.com/o/oauth2/v2/auth", | ||
| 60 | - | TriggerKind: port.TriggerScriptWindowOpen, | 61 | + | TriggerKind: dto.TriggerScriptWindowOpen, |
| 61 | - | TargetDisposition: port.WindowDispositionNewPopup, | 62 | + | TargetDisposition: dto.WindowDispositionNewPopup, |
| 62 | AuthIntent: true, | 63 | AuthIntent: true, | ||
| 63 | - | RequestContextDisposition: port.RequestContextInheritParent, | 64 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 64 | }, | 65 | }, | ||
| 65 | - | wantKind: port.HostDecisionCreateNativeWin, | 66 | + | wantKind: dto.HostDecisionCreateNativeWin, |
| 66 | }, | 67 | }, | ||
| 67 | { | 68 | { | ||
| 68 | name: "ambiguous opener-coupled request prefers native", | 69 | name: "ambiguous opener-coupled request prefers native", | ||
| 69 | - | request: port.NewBrowsingContextRequest{ | 70 | + | request: dto.NewBrowsingContextRequest{ |
| 70 | TargetURI: "https://example.com/popup", | 71 | TargetURI: "https://example.com/popup", | ||
| 71 | - | TriggerKind: port.TriggerUnknown, | 72 | + | TriggerKind: dto.TriggerUnknown, |
| 72 | - | TargetDisposition: port.WindowDispositionNewPopup, | 73 | + | TargetDisposition: dto.WindowDispositionNewPopup, |
| 73 | NoJavaScriptAccess: false, | 74 | NoJavaScriptAccess: false, | ||
| 74 | - | RequestContextDisposition: port.RequestContextInheritParent, | 75 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 75 | }, | 76 | }, | ||
| 76 | - | wantKind: port.HostDecisionCreateNativeWin, | 77 | + | wantKind: dto.HostDecisionCreateNativeWin, |
| 77 | }, | 78 | }, | ||
| 78 | { | 79 | { | ||
| 79 | name: "empty target is denied", | 80 | name: "empty target is denied", | ||
| 80 | - | request: port.NewBrowsingContextRequest{ | 81 | + | request: dto.NewBrowsingContextRequest{ |
| 81 | TargetURI: "", | 82 | TargetURI: "", | ||
| 82 | - | TriggerKind: port.TriggerLinkNewPage, | 83 | + | TriggerKind: dto.TriggerLinkNewPage, |
| 83 | - | RequestContextDisposition: port.RequestContextInheritParent, | 84 | + | RequestContextDisposition: dto.RequestContextInheritParent, |
| 84 | }, | 85 | }, | ||
| 85 | - | wantKind: port.HostDecisionDeny, | 86 | + | wantKind: dto.HostDecisionDeny, |
| 86 | }, | 87 | }, | ||
| 87 | } | 88 | } | ||
| 88 | 89 | ||||
| @@ -92,7 +93,7 @@ func TestBrowsingContextPolicyDecide(t *testing.T) { | |||||
| 92 | got := policy.Decide(tt.request, tt.namedContextExist) | 93 | got := policy.Decide(tt.request, tt.namedContextExist) | ||
| 93 | assert.Equal(t, tt.wantKind, got.Kind) | 94 | assert.Equal(t, tt.wantKind, got.Kind) | ||
| 94 | assert.Equal(t, tt.wantName, got.BrowsingContextName) | 95 | assert.Equal(t, tt.wantName, got.BrowsingContextName) | ||
| 95 | - | assert.Equal(t, port.RequestContextInheritParent, got.RequestContextDisposition) | 96 | + | assert.Equal(t, dto.RequestContextInheritParent, got.RequestContextDisposition) |
| 96 | }) | 97 | }) | ||
| 97 | } | 98 | } | ||
| 98 | } | 99 | } | ||
| @@ -103,10 +104,10 @@ func TestInferPopupTriggerKind_DoesNotTreatNoJavaScriptAccessFalseAsNativeSignal | |||||
| 103 | req := port.PopupRequest{ | 104 | req := port.PopupRequest{ | ||
| 104 | TargetURI: "https://example.com/window-open", | 105 | TargetURI: "https://example.com/window-open", | ||
| 105 | NoJavaScriptAccess: false, | 106 | NoJavaScriptAccess: false, | ||
| 106 | - | TargetDisposition: port.WindowDispositionNewPopup, | 107 | + | TargetDisposition: dto.WindowDispositionNewPopup, |
| 107 | } | 108 | } | ||
| 108 | 109 | ||||
| 109 | - | assert.Equal(t, port.TriggerScriptWindowOpen, inferPopupTriggerKind(req)) | 110 | + | assert.Equal(t, dto.TriggerScriptWindowOpen, inferPopupTriggerKind(req)) |
| 110 | } | 111 | } | ||
| 111 | 112 | ||||
| 112 | func TestInferPopupTriggerKind_ClassifiesCurrentTabAsNavigation(t *testing.T) { | 113 | func TestInferPopupTriggerKind_ClassifiesCurrentTabAsNavigation(t *testing.T) { | ||
| @@ -115,24 +116,24 @@ func TestInferPopupTriggerKind_ClassifiesCurrentTabAsNavigation(t *testing.T) { | |||||
| 115 | tests := []struct { | 116 | tests := []struct { | ||
| 116 | name string | 117 | name string | ||
| 117 | req port.PopupRequest | 118 | req port.PopupRequest | ||
| 118 | - | want port.TriggerKind | 119 | + | want dto.TriggerKind |
| 119 | }{ | 120 | }{ | ||
| 120 | { | 121 | { | ||
| 121 | name: "unnamed current-tab request stays a navigation", | 122 | name: "unnamed current-tab request stays a navigation", | ||
| 122 | req: port.PopupRequest{ | 123 | req: port.PopupRequest{ | ||
| 123 | TargetURI: "https://example.com/docs", | 124 | TargetURI: "https://example.com/docs", | ||
| 124 | - | TargetDisposition: port.WindowDispositionCurrentTab, | 125 | + | TargetDisposition: dto.WindowDispositionCurrentTab, |
| 125 | }, | 126 | }, | ||
| 126 | - | want: port.TriggerLinkNewPage, | 127 | + | want: dto.TriggerLinkNewPage, |
| 127 | }, | 128 | }, | ||
| 128 | { | 129 | { | ||
| 129 | name: "named current-tab request is treated as named target navigation", | 130 | name: "named current-tab request is treated as named target navigation", | ||
| 130 | req: port.PopupRequest{ | 131 | req: port.PopupRequest{ | ||
| 131 | TargetURI: "https://example.com/docs", | 132 | TargetURI: "https://example.com/docs", | ||
| 132 | FrameName: "shared-pane", | 133 | FrameName: "shared-pane", | ||
| 133 | - | TargetDisposition: port.WindowDispositionCurrentTab, | 134 | + | TargetDisposition: dto.WindowDispositionCurrentTab, |
| 134 | }, | 135 | }, | ||
| 135 | - | want: port.TriggerNamedTargetNavigation, | 136 | + | want: dto.TriggerNamedTargetNavigation, |
| 136 | }, | 137 | }, | ||
| 137 | } | 138 | } | ||
| 138 | 139 | ||||
| @@ -150,17 +151,17 @@ func TestInferPopupWindowDisposition_NormalizesBlankTarget(t *testing.T) { | |||||
| 150 | tests := []struct { | 151 | tests := []struct { | ||
| 151 | name string | 152 | name string | ||
| 152 | req port.PopupRequest | 153 | req port.PopupRequest | ||
| 153 | - | want port.WindowDisposition | 154 | + | want dto.WindowDisposition |
| 154 | }{ | 155 | }{ | ||
| 155 | { | 156 | { | ||
| 156 | name: "mixed-case blank target is treated as new tab", | 157 | name: "mixed-case blank target is treated as new tab", | ||
| 157 | req: port.PopupRequest{FrameName: " _BlAnK "}, | 158 | req: port.PopupRequest{FrameName: " _BlAnK "}, | ||
| 158 | - | want: port.WindowDispositionNewTab, | 159 | + | want: dto.WindowDispositionNewTab, |
| 159 | }, | 160 | }, | ||
| 160 | { | 161 | { | ||
| 161 | name: "named target still defaults to popup", | 162 | name: "named target still defaults to popup", | ||
| 162 | req: port.PopupRequest{FrameName: "shared-pane"}, | 163 | req: port.PopupRequest{FrameName: "shared-pane"}, | ||
| 163 | - | want: port.WindowDispositionNewPopup, | 164 | + | want: dto.WindowDispositionNewPopup, |
| 164 | }, | 165 | }, | ||
| 165 | } | 166 | } | ||
| 166 | 167 | ||||
M internal/ui/coordinator/content/oauth_test.go +34 -0
| @@ -148,6 +148,40 @@ func TestObserveNativePopupAuth_DoesNotTrackWhenWebViewLacksOAuthCallbacks(t *te | |||
| 148 | 148 | assert.False(t, ok) | |
| 149 | 149 | } | |
| 150 | 150 | ||
| 151 | + | func TestObserveNativePopupAuth_SchedulesCloseOnlyOnce(t *testing.T) { | |
| 152 | + | popupID := port.WebViewID(1002) | |
| 153 | + | wv := &popupOAuthAutoCloseWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | |
| 154 | + | wv.EXPECT().ID().Return(popupID).Once() | |
| 155 | + | wv.EXPECT().IsDestroyed().Return(false).Once() | |
| 156 | + | ||
| 157 | + | oldSchedule := scheduleNativePopupOAuthClose | |
| 158 | + | t.Cleanup(func() { | |
| 159 | + | scheduleNativePopupOAuthClose = oldSchedule | |
| 160 | + | }) | |
| 161 | + | ||
| 162 | + | scheduledCalls := 0 | |
| 163 | + | scheduleNativePopupOAuthClose = func(delay time.Duration, fn func()) { | |
| 164 | + | scheduledCalls++ | |
| 165 | + | assert.Equal(t, nativePopupOAuthCloseDelay, delay) | |
| 166 | + | fn() | |
| 167 | + | } | |
| 168 | + | ||
| 169 | + | c := &Coordinator{popups: newPopupManager()} | |
| 170 | + | c.ObserveNativePopupAuth(context.Background(), NativePopupInput{ | |
| 171 | + | ParentPaneID: entity.PaneID("parent-pane"), | |
| 172 | + | ParentURIAtOpen: "https://x.com/i/flow/login", | |
| 173 | + | PopupWebView: wv, | |
| 174 | + | TargetURI: "https://accounts.google.com/o/oauth2/v2/auth", | |
| 175 | + | }) | |
| 176 | + | require.Len(t, wv.navigationCallbacks, 1) | |
| 177 | + | ||
| 178 | + | wv.navigationCallbacks[0]("https://x.com/googlepopupcallback?code=123") | |
| 179 | + | wv.navigationCallbacks[0]("https://x.com/googlepopupcallback?code=123") | |
| 180 | + | ||
| 181 | + | assert.Equal(t, 1, scheduledCalls) | |
| 182 | + | assert.Equal(t, 1, wv.closeCount) | |
| 183 | + | } | |
| 184 | + | ||
| 151 | 185 | func TestSetupOAuthAutoClose_NavigationIgnoresNonTerminalURI(t *testing.T) { | |
| 152 | 186 | popupID := port.WebViewID(1000) | |
| 153 | 187 | wv := &popupOAuthAutoCloseWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | |
| @@ -148,6 +148,40 @@ func TestObserveNativePopupAuth_DoesNotTrackWhenWebViewLacksOAuthCallbacks(t *te | |||||
| 148 | assert.False(t, ok) | 148 | assert.False(t, ok) | ||
| 149 | } | 149 | } | ||
| 150 | 150 | ||||
| 151 | + | func TestObserveNativePopupAuth_SchedulesCloseOnlyOnce(t *testing.T) { | |||
| 152 | + | popupID := port.WebViewID(1002) | |||
| 153 | + | wv := &popupOAuthAutoCloseWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | |||
| 154 | + | wv.EXPECT().ID().Return(popupID).Once() | |||
| 155 | + | wv.EXPECT().IsDestroyed().Return(false).Once() | |||
| 156 | + | ||||
| 157 | + | oldSchedule := scheduleNativePopupOAuthClose | |||
| 158 | + | t.Cleanup(func() { | |||
| 159 | + | scheduleNativePopupOAuthClose = oldSchedule | |||
| 160 | + | }) | |||
| 161 | + | ||||
| 162 | + | scheduledCalls := 0 | |||
| 163 | + | scheduleNativePopupOAuthClose = func(delay time.Duration, fn func()) { | |||
| 164 | + | scheduledCalls++ | |||
| 165 | + | assert.Equal(t, nativePopupOAuthCloseDelay, delay) | |||
| 166 | + | fn() | |||
| 167 | + | } | |||
| 168 | + | ||||
| 169 | + | c := &Coordinator{popups: newPopupManager()} | |||
| 170 | + | c.ObserveNativePopupAuth(context.Background(), NativePopupInput{ | |||
| 171 | + | ParentPaneID: entity.PaneID("parent-pane"), | |||
| 172 | + | ParentURIAtOpen: "https://x.com/i/flow/login", | |||
| 173 | + | PopupWebView: wv, | |||
| 174 | + | TargetURI: "https://accounts.google.com/o/oauth2/v2/auth", | |||
| 175 | + | }) | |||
| 176 | + | require.Len(t, wv.navigationCallbacks, 1) | |||
| 177 | + | ||||
| 178 | + | wv.navigationCallbacks[0]("https://x.com/googlepopupcallback?code=123") | |||
| 179 | + | wv.navigationCallbacks[0]("https://x.com/googlepopupcallback?code=123") | |||
| 180 | + | ||||
| 181 | + | assert.Equal(t, 1, scheduledCalls) | |||
| 182 | + | assert.Equal(t, 1, wv.closeCount) | |||
| 183 | + | } | |||
| 184 | + | ||||
| 151 | func TestSetupOAuthAutoClose_NavigationIgnoresNonTerminalURI(t *testing.T) { | 185 | func TestSetupOAuthAutoClose_NavigationIgnoresNonTerminalURI(t *testing.T) { | ||
| 152 | popupID := port.WebViewID(1000) | 186 | popupID := port.WebViewID(1000) | ||
| 153 | wv := &popupOAuthAutoCloseWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | 187 | wv := &popupOAuthAutoCloseWebViewStub{MockWebView: portmocks.NewMockWebView(t)} | ||
M internal/ui/coordinator/content/popup_manager.go +8 -7
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "sync" | |
| 8 | 8 | "time" | |
| 9 | 9 | ||
| 10 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 10 | 11 | "github.com/bnema/dumber/internal/application/port" | |
| 11 | 12 | "github.com/bnema/dumber/internal/domain/entity" | |
| 12 | 13 | "github.com/bnema/dumber/internal/logging" | |
| @@ -122,7 +123,7 @@ func popupTabInsertionConfig(cfg *entity.BrowsingContextConfig) (entity.PopupBeh | |||
| 122 | 123 | return behavior, placement | |
| 123 | 124 | } | |
| 124 | 125 | ||
| 125 | - | func (*popupManager) setBrowsingContextDecision(wv port.WebView, decision port.HostDecision) { | |
| 126 | + | func (*popupManager) setBrowsingContextDecision(wv port.WebView, decision dto.HostDecision) { | |
| 126 | 127 | if carrier, ok := wv.(port.BrowsingContextHostDecisionCapable); ok { | |
| 127 | 128 | carrier.SetBrowsingContextHostDecision(decision) | |
| 128 | 129 | } | |
| @@ -426,7 +427,7 @@ func (pm *popupManager) openNativePopup( | |||
| 426 | 427 | parentID port.WebViewID, | |
| 427 | 428 | parentURIAtOpen string, | |
| 428 | 429 | req port.PopupRequest, | |
| 429 | - | decision port.HostDecision, | |
| 430 | + | decision dto.HostDecision, | |
| 430 | 431 | ) port.WebView { | |
| 431 | 432 | log := logging.FromContext(ctx) | |
| 432 | 433 | if pm.onOpenNativePopup == nil { | |
| @@ -500,9 +501,9 @@ func (pm *popupManager) handlePopupCreate( | |||
| 500 | 501 | Msg("browsing context host decision") | |
| 501 | 502 | ||
| 502 | 503 | switch decision.Kind { | |
| 503 | - | case port.HostDecisionDeny: | |
| 504 | + | case dto.HostDecisionDeny: | |
| 504 | 505 | return nil | |
| 505 | - | case port.HostDecisionReuseNamedPane: | |
| 506 | + | case dto.HostDecisionReuseNamedPane: | |
| 506 | 507 | if req.NoJavaScriptAccess { | |
| 507 | 508 | log.Warn().Str("frame_name", req.FrameName).Msg("named browsing context reuse denied for noopener popup") | |
| 508 | 509 | return nil | |
| @@ -514,9 +515,9 @@ func (pm *popupManager) handlePopupCreate( | |||
| 514 | 515 | } | |
| 515 | 516 | pm.setBrowsingContextDecision(reused, decision) | |
| 516 | 517 | return reused | |
| 517 | - | case port.HostDecisionCreateNativeWin: | |
| 518 | + | case dto.HostDecisionCreateNativeWin: | |
| 518 | 519 | return pm.openNativePopup(ctx, parentPaneID, parentID, parentURIAtOpen, req, decision) | |
| 519 | - | case port.HostDecisionCreatePane: | |
| 520 | + | case dto.HostDecisionCreatePane: | |
| 520 | 521 | // Continue below. | |
| 521 | 522 | break | |
| 522 | 523 | } | |
| @@ -812,7 +813,7 @@ func (pm *popupManager) handleLinkMiddleClick( | |||
| 812 | 813 | Str("decision", string(decision.Kind)). | |
| 813 | 814 | Str("reason", decision.Reason). | |
| 814 | 815 | Msg("middle-click browsing context host decision") | |
| 815 | - | if decision.Kind != port.HostDecisionCreatePane { | |
| 816 | + | if decision.Kind != dto.HostDecisionCreatePane { | |
| 816 | 817 | log.Info().Str("decision", string(decision.Kind)).Msg("middle-click browsing context not pane-hosted") | |
| 817 | 818 | return false | |
| 818 | 819 | } | |
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "sync" | 7 | "sync" | ||
| 8 | "time" | 8 | "time" | ||
| 9 | 9 | ||||
| 10 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 10 | "github.com/bnema/dumber/internal/application/port" | 11 | "github.com/bnema/dumber/internal/application/port" | ||
| 11 | "github.com/bnema/dumber/internal/domain/entity" | 12 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 12 | "github.com/bnema/dumber/internal/logging" | 13 | "github.com/bnema/dumber/internal/logging" | ||
| @@ -122,7 +123,7 @@ func popupTabInsertionConfig(cfg *entity.BrowsingContextConfig) (entity.PopupBeh | |||||
| 122 | return behavior, placement | 123 | return behavior, placement | ||
| 123 | } | 124 | } | ||
| 124 | 125 | ||||
| 125 | - | func (*popupManager) setBrowsingContextDecision(wv port.WebView, decision port.HostDecision) { | 126 | + | func (*popupManager) setBrowsingContextDecision(wv port.WebView, decision dto.HostDecision) { |
| 126 | if carrier, ok := wv.(port.BrowsingContextHostDecisionCapable); ok { | 127 | if carrier, ok := wv.(port.BrowsingContextHostDecisionCapable); ok { | ||
| 127 | carrier.SetBrowsingContextHostDecision(decision) | 128 | carrier.SetBrowsingContextHostDecision(decision) | ||
| 128 | } | 129 | } | ||
| @@ -426,7 +427,7 @@ func (pm *popupManager) openNativePopup( | |||||
| 426 | parentID port.WebViewID, | 427 | parentID port.WebViewID, | ||
| 427 | parentURIAtOpen string, | 428 | parentURIAtOpen string, | ||
| 428 | req port.PopupRequest, | 429 | req port.PopupRequest, | ||
| 429 | - | decision port.HostDecision, | 430 | + | decision dto.HostDecision, |
| 430 | ) port.WebView { | 431 | ) port.WebView { | ||
| 431 | log := logging.FromContext(ctx) | 432 | log := logging.FromContext(ctx) | ||
| 432 | if pm.onOpenNativePopup == nil { | 433 | if pm.onOpenNativePopup == nil { | ||
| @@ -500,9 +501,9 @@ func (pm *popupManager) handlePopupCreate( | |||||
| 500 | Msg("browsing context host decision") | 501 | Msg("browsing context host decision") | ||
| 501 | 502 | ||||
| 502 | switch decision.Kind { | 503 | switch decision.Kind { | ||
| 503 | - | case port.HostDecisionDeny: | 504 | + | case dto.HostDecisionDeny: |
| 504 | return nil | 505 | return nil | ||
| 505 | - | case port.HostDecisionReuseNamedPane: | 506 | + | case dto.HostDecisionReuseNamedPane: |
| 506 | if req.NoJavaScriptAccess { | 507 | if req.NoJavaScriptAccess { | ||
| 507 | log.Warn().Str("frame_name", req.FrameName).Msg("named browsing context reuse denied for noopener popup") | 508 | log.Warn().Str("frame_name", req.FrameName).Msg("named browsing context reuse denied for noopener popup") | ||
| 508 | return nil | 509 | return nil | ||
| @@ -514,9 +515,9 @@ func (pm *popupManager) handlePopupCreate( | |||||
| 514 | } | 515 | } | ||
| 515 | pm.setBrowsingContextDecision(reused, decision) | 516 | pm.setBrowsingContextDecision(reused, decision) | ||
| 516 | return reused | 517 | return reused | ||
| 517 | - | case port.HostDecisionCreateNativeWin: | 518 | + | case dto.HostDecisionCreateNativeWin: |
| 518 | return pm.openNativePopup(ctx, parentPaneID, parentID, parentURIAtOpen, req, decision) | 519 | return pm.openNativePopup(ctx, parentPaneID, parentID, parentURIAtOpen, req, decision) | ||
| 519 | - | case port.HostDecisionCreatePane: | 520 | + | case dto.HostDecisionCreatePane: |
| 520 | // Continue below. | 521 | // Continue below. | ||
| 521 | break | 522 | break | ||
| 522 | } | 523 | } | ||
| @@ -812,7 +813,7 @@ func (pm *popupManager) handleLinkMiddleClick( | |||||
| 812 | Str("decision", string(decision.Kind)). | 813 | Str("decision", string(decision.Kind)). | ||
| 813 | Str("reason", decision.Reason). | 814 | Str("reason", decision.Reason). | ||
| 814 | Msg("middle-click browsing context host decision") | 815 | Msg("middle-click browsing context host decision") | ||
| 815 | - | if decision.Kind != port.HostDecisionCreatePane { | 816 | + | if decision.Kind != dto.HostDecisionCreatePane { |
| 816 | log.Info().Str("decision", string(decision.Kind)).Msg("middle-click browsing context not pane-hosted") | 817 | log.Info().Str("decision", string(decision.Kind)).Msg("middle-click browsing context not pane-hosted") | ||
| 817 | return false | 818 | return false | ||
| 818 | } | 819 | } | ||
M internal/ui/coordinator/content/popup_test.go +14 -4
| @@ -10,6 +10,7 @@ import ( | |||
| 10 | 10 | "github.com/stretchr/testify/mock" | |
| 11 | 11 | "github.com/stretchr/testify/require" | |
| 12 | 12 | ||
| 13 | + | "github.com/bnema/dumber/internal/application/dto" | |
| 13 | 14 | "github.com/bnema/dumber/internal/application/port" | |
| 14 | 15 | "github.com/bnema/dumber/internal/application/port/mocks" | |
| 15 | 16 | "github.com/bnema/dumber/internal/domain/entity" | |
| @@ -361,7 +362,7 @@ func TestHandleLinkMiddleClick_UsesRelatedWebView(t *testing.T) { | |||
| 361 | 362 | assert.Equal(t, 1, insertCalls) | |
| 362 | 363 | decision, hasDecision := newWV.BrowsingContextHostDecision() | |
| 363 | 364 | require.True(t, hasDecision) | |
| 364 | - | assert.Equal(t, port.HostDecisionCreatePane, decision.Kind) | |
| 365 | + | assert.Equal(t, dto.HostDecisionCreatePane, decision.Kind) | |
| 365 | 366 | } | |
| 366 | 367 | ||
| 367 | 368 | func TestHandlePopupCreate_OpensNativePopupForAuthIntent(t *testing.T) { | |
| @@ -421,12 +422,19 @@ func TestHandlePopupCreate_NativePopupDoesNotForceOAuthObservationWhenDisabled(t | |||
| 421 | 422 | factory := mocks.NewMockWebViewFactory(t) | |
| 422 | 423 | factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once() | |
| 423 | 424 | ||
| 425 | + | insertCalls := 0 | |
| 426 | + | nativeCalls := 0 | |
| 424 | 427 | c := &Coordinator{ | |
| 425 | 428 | webViews: make(map[entity.PaneID]port.WebView), | |
| 426 | 429 | popups: newPopupManager(), | |
| 427 | 430 | } | |
| 428 | 431 | c.SetPopupConfig(factory, &entity.BrowsingContextConfig{OpenInNewPane: true, OAuthAutoClose: false}, nil) | |
| 432 | + | c.SetOnInsertPopup(func(context.Context, InsertPopupInput) error { | |
| 433 | + | insertCalls++ | |
| 434 | + | return nil | |
| 435 | + | }) | |
| 429 | 436 | c.SetOnOpenNativePopup(func(_ context.Context, input NativePopupInput) error { | |
| 437 | + | nativeCalls++ | |
| 430 | 438 | assert.False(t, input.ObserveOAuthAutoClose) | |
| 431 | 439 | return nil | |
| 432 | 440 | }) | |
| @@ -438,6 +446,8 @@ func TestHandlePopupCreate_NativePopupDoesNotForceOAuthObservationWhenDisabled(t | |||
| 438 | 446 | }) | |
| 439 | 447 | ||
| 440 | 448 | require.Equal(t, popupWV, created) | |
| 449 | + | assert.Equal(t, 1, nativeCalls) | |
| 450 | + | assert.Equal(t, 0, insertCalls) | |
| 441 | 451 | } | |
| 442 | 452 | ||
| 443 | 453 | func TestHandlePopupCreate_DoesNotReuseNamedPopupWhenNoJavaScriptAccess(t *testing.T) { | |
| @@ -678,7 +688,7 @@ func TestSetPopupConfig_NilConfigAllowed(t *testing.T) { | |||
| 678 | 688 | type popupNavigationWebViewStub struct { | |
| 679 | 689 | *mocks.MockWebView | |
| 680 | 690 | primed []string | |
| 681 | - | browsingContextDecision port.HostDecision | |
| 691 | + | browsingContextDecision dto.HostDecision | |
| 682 | 692 | hasBrowsingContextDecision bool | |
| 683 | 693 | } | |
| 684 | 694 | ||
| @@ -686,12 +696,12 @@ func (s *popupNavigationWebViewStub) PrimePopupNavigation(uri string) { | |||
| 686 | 696 | s.primed = append(s.primed, uri) | |
| 687 | 697 | } | |
| 688 | 698 | ||
| 689 | - | func (s *popupNavigationWebViewStub) SetBrowsingContextHostDecision(decision port.HostDecision) { | |
| 699 | + | func (s *popupNavigationWebViewStub) SetBrowsingContextHostDecision(decision dto.HostDecision) { | |
| 690 | 700 | s.browsingContextDecision = decision | |
| 691 | 701 | s.hasBrowsingContextDecision = true | |
| 692 | 702 | } | |
| 693 | 703 | ||
| 694 | - | func (s *popupNavigationWebViewStub) BrowsingContextHostDecision() (port.HostDecision, bool) { | |
| 704 | + | func (s *popupNavigationWebViewStub) BrowsingContextHostDecision() (dto.HostDecision, bool) { | |
| 695 | 705 | return s.browsingContextDecision, s.hasBrowsingContextDecision | |
| 696 | 706 | } | |
| 697 | 707 | ||
| @@ -10,6 +10,7 @@ import ( | |||||
| 10 | "github.com/stretchr/testify/mock" | 10 | "github.com/stretchr/testify/mock" | ||
| 11 | "github.com/stretchr/testify/require" | 11 | "github.com/stretchr/testify/require" | ||
| 12 | 12 | ||||
| 13 | + | "github.com/bnema/dumber/internal/application/dto" | |||
| 13 | "github.com/bnema/dumber/internal/application/port" | 14 | "github.com/bnema/dumber/internal/application/port" | ||
| 14 | "github.com/bnema/dumber/internal/application/port/mocks" | 15 | "github.com/bnema/dumber/internal/application/port/mocks" | ||
| 15 | "github.com/bnema/dumber/internal/domain/entity" | 16 | "github.com/bnema/dumber/internal/domain/entity" | ||
| @@ -361,7 +362,7 @@ func TestHandleLinkMiddleClick_UsesRelatedWebView(t *testing.T) { | |||||
| 361 | assert.Equal(t, 1, insertCalls) | 362 | assert.Equal(t, 1, insertCalls) | ||
| 362 | decision, hasDecision := newWV.BrowsingContextHostDecision() | 363 | decision, hasDecision := newWV.BrowsingContextHostDecision() | ||
| 363 | require.True(t, hasDecision) | 364 | require.True(t, hasDecision) | ||
| 364 | - | assert.Equal(t, port.HostDecisionCreatePane, decision.Kind) | 365 | + | assert.Equal(t, dto.HostDecisionCreatePane, decision.Kind) |
| 365 | } | 366 | } | ||
| 366 | 367 | ||||
| 367 | func TestHandlePopupCreate_OpensNativePopupForAuthIntent(t *testing.T) { | 368 | func TestHandlePopupCreate_OpensNativePopupForAuthIntent(t *testing.T) { | ||
| @@ -421,12 +422,19 @@ func TestHandlePopupCreate_NativePopupDoesNotForceOAuthObservationWhenDisabled(t | |||||
| 421 | factory := mocks.NewMockWebViewFactory(t) | 422 | factory := mocks.NewMockWebViewFactory(t) | ||
| 422 | factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once() | 423 | factory.EXPECT().CreateRelated(mock.Anything, port.WebViewID(101)).Return(popupWV, nil).Once() | ||
| 423 | 424 | ||||
| 425 | + | insertCalls := 0 | |||
| 426 | + | nativeCalls := 0 | |||
| 424 | c := &Coordinator{ | 427 | c := &Coordinator{ | ||
| 425 | webViews: make(map[entity.PaneID]port.WebView), | 428 | webViews: make(map[entity.PaneID]port.WebView), | ||
| 426 | popups: newPopupManager(), | 429 | popups: newPopupManager(), | ||
| 427 | } | 430 | } | ||
| 428 | c.SetPopupConfig(factory, &entity.BrowsingContextConfig{OpenInNewPane: true, OAuthAutoClose: false}, nil) | 431 | c.SetPopupConfig(factory, &entity.BrowsingContextConfig{OpenInNewPane: true, OAuthAutoClose: false}, nil) | ||
| 432 | + | c.SetOnInsertPopup(func(context.Context, InsertPopupInput) error { | |||
| 433 | + | insertCalls++ | |||
| 434 | + | return nil | |||
| 435 | + | }) | |||
| 429 | c.SetOnOpenNativePopup(func(_ context.Context, input NativePopupInput) error { | 436 | c.SetOnOpenNativePopup(func(_ context.Context, input NativePopupInput) error { | ||
| 437 | + | nativeCalls++ | |||
| 430 | assert.False(t, input.ObserveOAuthAutoClose) | 438 | assert.False(t, input.ObserveOAuthAutoClose) | ||
| 431 | return nil | 439 | return nil | ||
| 432 | }) | 440 | }) | ||
| @@ -438,6 +446,8 @@ func TestHandlePopupCreate_NativePopupDoesNotForceOAuthObservationWhenDisabled(t | |||||
| 438 | }) | 446 | }) | ||
| 439 | 447 | ||||
| 440 | require.Equal(t, popupWV, created) | 448 | require.Equal(t, popupWV, created) | ||
| 449 | + | assert.Equal(t, 1, nativeCalls) | |||
| 450 | + | assert.Equal(t, 0, insertCalls) | |||
| 441 | } | 451 | } | ||
| 442 | 452 | ||||
| 443 | func TestHandlePopupCreate_DoesNotReuseNamedPopupWhenNoJavaScriptAccess(t *testing.T) { | 453 | func TestHandlePopupCreate_DoesNotReuseNamedPopupWhenNoJavaScriptAccess(t *testing.T) { | ||
| @@ -678,7 +688,7 @@ func TestSetPopupConfig_NilConfigAllowed(t *testing.T) { | |||||
| 678 | type popupNavigationWebViewStub struct { | 688 | type popupNavigationWebViewStub struct { | ||
| 679 | *mocks.MockWebView | 689 | *mocks.MockWebView | ||
| 680 | primed []string | 690 | primed []string | ||
| 681 | - | browsingContextDecision port.HostDecision | 691 | + | browsingContextDecision dto.HostDecision |
| 682 | hasBrowsingContextDecision bool | 692 | hasBrowsingContextDecision bool | ||
| 683 | } | 693 | } | ||
| 684 | 694 | ||||
| @@ -686,12 +696,12 @@ func (s *popupNavigationWebViewStub) PrimePopupNavigation(uri string) { | |||||
| 686 | s.primed = append(s.primed, uri) | 696 | s.primed = append(s.primed, uri) | ||
| 687 | } | 697 | } | ||
| 688 | 698 | ||||
| 689 | - | func (s *popupNavigationWebViewStub) SetBrowsingContextHostDecision(decision port.HostDecision) { | 699 | + | func (s *popupNavigationWebViewStub) SetBrowsingContextHostDecision(decision dto.HostDecision) { |
| 690 | s.browsingContextDecision = decision | 700 | s.browsingContextDecision = decision | ||
| 691 | s.hasBrowsingContextDecision = true | 701 | s.hasBrowsingContextDecision = true | ||
| 692 | } | 702 | } | ||
| 693 | 703 | ||||
| 694 | - | func (s *popupNavigationWebViewStub) BrowsingContextHostDecision() (port.HostDecision, bool) { | 704 | + | func (s *popupNavigationWebViewStub) BrowsingContextHostDecision() (dto.HostDecision, bool) { |
| 695 | return s.browsingContextDecision, s.hasBrowsingContextDecision | 705 | return s.browsingContextDecision, s.hasBrowsingContextDecision | ||
| 696 | } | 706 | } | ||
| 697 | 707 | ||||
M internal/ui/native_popup_window.go +4 -1
| @@ -27,6 +27,8 @@ type nativePopupDestroyer interface { | |||
| 27 | 27 | Destroy() | |
| 28 | 28 | } | |
| 29 | 29 | ||
| 30 | + | var newPopupWindow = window.NewPopup | |
| 31 | + | ||
| 30 | 32 | func destroyFailedNativePopupSetup(popupShell nativePopupDestroyer, wv port.WebView) { | |
| 31 | 33 | if wv != nil && !wv.IsDestroyed() { | |
| 32 | 34 | wv.Destroy() | |
| @@ -63,8 +65,9 @@ func (a *App) openNativePopupWindow(ctx context.Context, input content.NativePop | |||
| 63 | 65 | return fmt.Errorf("native popup webview is nil") | |
| 64 | 66 | } | |
| 65 | 67 | ||
| 66 | - | popupShell, err := window.NewPopup(ctx, a.gtkApp) | |
| 68 | + | popupShell, err := newPopupWindow(ctx, a.gtkApp) | |
| 67 | 69 | if err != nil { | |
| 70 | + | destroyFailedNativePopupSetup(nil, input.PopupWebView) | |
| 68 | 71 | return err | |
| 69 | 72 | } | |
| 70 | 73 | if a.contentCoord == nil { | |
| @@ -27,6 +27,8 @@ type nativePopupDestroyer interface { | |||||
| 27 | Destroy() | 27 | Destroy() | ||
| 28 | } | 28 | } | ||
| 29 | 29 | ||||
| 30 | + | var newPopupWindow = window.NewPopup | |||
| 31 | + | ||||
| 30 | func destroyFailedNativePopupSetup(popupShell nativePopupDestroyer, wv port.WebView) { | 32 | func destroyFailedNativePopupSetup(popupShell nativePopupDestroyer, wv port.WebView) { | ||
| 31 | if wv != nil && !wv.IsDestroyed() { | 33 | if wv != nil && !wv.IsDestroyed() { | ||
| 32 | wv.Destroy() | 34 | wv.Destroy() | ||
| @@ -63,8 +65,9 @@ func (a *App) openNativePopupWindow(ctx context.Context, input content.NativePop | |||||
| 63 | return fmt.Errorf("native popup webview is nil") | 65 | return fmt.Errorf("native popup webview is nil") | ||
| 64 | } | 66 | } | ||
| 65 | 67 | ||||
| 66 | - | popupShell, err := window.NewPopup(ctx, a.gtkApp) | 68 | + | popupShell, err := newPopupWindow(ctx, a.gtkApp) |
| 67 | if err != nil { | 69 | if err != nil { | ||
| 70 | + | destroyFailedNativePopupSetup(nil, input.PopupWebView) | |||
| 68 | return err | 71 | return err | ||
| 69 | } | 72 | } | ||
| 70 | if a.contentCoord == nil { | 73 | if a.contentCoord == nil { | ||
M internal/ui/native_popup_window_test.go +24 -0
| @@ -1,11 +1,15 @@ | |||
| 1 | 1 | package ui | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "context" | |
| 5 | + | "errors" | |
| 4 | 6 | "testing" | |
| 5 | 7 | ||
| 6 | 8 | "github.com/bnema/dumber/internal/application/port" | |
| 7 | 9 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | |
| 10 | + | "github.com/bnema/dumber/internal/ui/coordinator/content" | |
| 8 | 11 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | |
| 12 | + | "github.com/bnema/dumber/internal/ui/window" | |
| 9 | 13 | "github.com/bnema/puregotk/v4/gtk" | |
| 10 | 14 | "github.com/stretchr/testify/assert" | |
| 11 | 15 | "github.com/stretchr/testify/require" | |
| @@ -40,6 +44,26 @@ func TestPrepareNativePopupContentWidget_ErrorsWhenWrappedGTKWidgetMissing(t *te | |||
| 40 | 44 | assert.Nil(t, got) | |
| 41 | 45 | } | |
| 42 | 46 | ||
| 47 | + | func TestOpenNativePopupWindow_DestroysWebViewWhenPopupShellCreationFails(t *testing.T) { | |
| 48 | + | oldNewPopupWindow := newPopupWindow | |
| 49 | + | t.Cleanup(func() { | |
| 50 | + | newPopupWindow = oldNewPopupWindow | |
| 51 | + | }) | |
| 52 | + | ||
| 53 | + | expectedErr := errors.New("boom") | |
| 54 | + | newPopupWindow = func(context.Context, *gtk.Application) (*window.PopupWindow, error) { | |
| 55 | + | return nil, expectedErr | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | wv := portmocks.NewMockWebView(t) | |
| 59 | + | wv.EXPECT().IsDestroyed().Return(false).Once() | |
| 60 | + | wv.EXPECT().Destroy().Once() | |
| 61 | + | ||
| 62 | + | app := &App{gtkApp: >k.Application{}} | |
| 63 | + | err := app.openNativePopupWindow(context.Background(), content.NativePopupInput{PopupWebView: wv}) | |
| 64 | + | require.ErrorIs(t, err, expectedErr) | |
| 65 | + | } | |
| 66 | + | ||
| 43 | 67 | func TestDestroyFailedNativePopupSetup_DestroysWebViewAndShell(t *testing.T) { | |
| 44 | 68 | wv := portmocks.NewMockWebView(t) | |
| 45 | 69 | wv.EXPECT().IsDestroyed().Return(false).Once() | |
| @@ -1,11 +1,15 @@ | |||||
| 1 | package ui | 1 | package ui | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "context" | |||
| 5 | + | "errors" | |||
| 4 | "testing" | 6 | "testing" | ||
| 5 | 7 | ||||
| 6 | "github.com/bnema/dumber/internal/application/port" | 8 | "github.com/bnema/dumber/internal/application/port" | ||
| 7 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | 9 | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | ||
| 10 | + | "github.com/bnema/dumber/internal/ui/coordinator/content" | |||
| 8 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | 11 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | ||
| 12 | + | "github.com/bnema/dumber/internal/ui/window" | |||
| 9 | "github.com/bnema/puregotk/v4/gtk" | 13 | "github.com/bnema/puregotk/v4/gtk" | ||
| 10 | "github.com/stretchr/testify/assert" | 14 | "github.com/stretchr/testify/assert" | ||
| 11 | "github.com/stretchr/testify/require" | 15 | "github.com/stretchr/testify/require" | ||
| @@ -40,6 +44,26 @@ func TestPrepareNativePopupContentWidget_ErrorsWhenWrappedGTKWidgetMissing(t *te | |||||
| 40 | assert.Nil(t, got) | 44 | assert.Nil(t, got) | ||
| 41 | } | 45 | } | ||
| 42 | 46 | ||||
| 47 | + | func TestOpenNativePopupWindow_DestroysWebViewWhenPopupShellCreationFails(t *testing.T) { | |||
| 48 | + | oldNewPopupWindow := newPopupWindow | |||
| 49 | + | t.Cleanup(func() { | |||
| 50 | + | newPopupWindow = oldNewPopupWindow | |||
| 51 | + | }) | |||
| 52 | + | ||||
| 53 | + | expectedErr := errors.New("boom") | |||
| 54 | + | newPopupWindow = func(context.Context, *gtk.Application) (*window.PopupWindow, error) { | |||
| 55 | + | return nil, expectedErr | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | wv := portmocks.NewMockWebView(t) | |||
| 59 | + | wv.EXPECT().IsDestroyed().Return(false).Once() | |||
| 60 | + | wv.EXPECT().Destroy().Once() | |||
| 61 | + | ||||
| 62 | + | app := &App{gtkApp: >k.Application{}} | |||
| 63 | + | err := app.openNativePopupWindow(context.Background(), content.NativePopupInput{PopupWebView: wv}) | |||
| 64 | + | require.ErrorIs(t, err, expectedErr) | |||
| 65 | + | } | |||
| 66 | + | ||||
| 43 | func TestDestroyFailedNativePopupSetup_DestroysWebViewAndShell(t *testing.T) { | 67 | func TestDestroyFailedNativePopupSetup_DestroysWebViewAndShell(t *testing.T) { | ||
| 44 | wv := portmocks.NewMockWebView(t) | 68 | wv := portmocks.NewMockWebView(t) | ||
| 45 | wv.EXPECT().IsDestroyed().Return(false).Once() | 69 | wv.EXPECT().IsDestroyed().Return(false).Once() | ||