external_url_relay_reuse_test.go RawHistoryFolder
1package ui
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/dumber/internal/application/port"
9 "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/domain/entity"
11 domainurl "github.com/bnema/dumber/internal/domain/url"
12 "github.com/bnema/dumber/internal/infrastructure/config"
13 "github.com/bnema/dumber/internal/ui/component"
14 "github.com/bnema/dumber/internal/ui/coordinator"
15 contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18)
19
20const relayTestURL = "https://claude.com/cai/login/app-google-auth?open_in_browser=1&hop_nonce=XYZ"
21
22type relayReuseHarness struct {
23 app *App
24 tabs *entity.TabList
25 window *browserWindow
26 webview map[entity.PaneID]*recordingWebView
27 clock *time.Time
28}
29
30// newRelayReuseHarness builds an App with one window holding one tab, using the
31// tabbed external-link behavior. New tabs get a recording webview registered so
32// reuse can navigate them.
33func newRelayReuseHarness(t *testing.T) *relayReuseHarness {
34 t.Helper()
35 ctx := context.Background()
36
37 original := entity.NewTab("tab-1", "ws-1", entity.NewPane("pane-1"))
38 tabs := entity.NewTabList()
39 tabs.Add(original)
40 window := &browserWindow{id: "focused", tabs: tabs}
41
42 ids := []string{"tab-2", "ws-2", "pane-2", "tab-3", "ws-3", "pane-3"}
43 index := 0
44 now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
45 h := &relayReuseHarness{tabs: tabs, window: window, webview: map[entity.PaneID]*recordingWebView{}, clock: &now}
46
47 contentCoord := contentcoord.NewCoordinator(ctx, nil, nil, nil, nil, nil, nil, nil)
48 app := &App{
49 runtimeConfig: runtimeConfigStateForTest(&config.Config{Workspace: entity.WorkspaceConfig{ExternalLinks: entity.ExternalLinksConfig{Behavior: entity.ExternalLinkBehaviorTabbed}}}),
50 tabsUC: usecase.NewManageTabsUseCase(func() string { id := ids[index]; index++; return id }, nil),
51 browserWindows: map[string]*browserWindow{
52 window.id: window,
53 },
54 browserWindowOrder: []string{window.id},
55 lastFocusedWindowID: window.id,
56 workspaceViews: map[entity.TabID]*component.WorkspaceView{},
57 windowForTab: map[entity.TabID]*browserWindow{},
58 contentCoord: contentCoord,
59 navCoord: coordinator.NewNavigationCoordinator(ctx, nil, contentCoord),
60 }
61 app.externalRelayPanes.Now = func() time.Time { return *h.clock }
62 app.tabCoord = coordinator.NewTabCoordinator(ctx, coordinator.TabCoordinatorConfig{TabsUC: app.tabsUC})
63 app.tabCoord.SetOnTabCreated(func(_ context.Context, _ coordinator.TabTarget, tab *entity.Tab) {
64 app.workspaceViews[tab.ID] = &component.WorkspaceView{}
65 app.setBrowserWindowForTab(tab.ID, window)
66 paneID := tab.Workspace.ActivePane().Pane.ID
67 wv := &recordingWebView{id: port.WebViewID(100 + len(h.webview))}
68 h.webview[paneID] = wv
69 contentCoord.RegisterPopupWebView(paneID, wv)
70 })
71 h.app = app
72 return h
73}
74
75func TestApp_OpenExternalURLReusesRecentRelayPane(t *testing.T) {
76 ctx := context.Background()
77 h := newRelayReuseHarness(t)
78
79 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
80 require.Len(t, h.tabs.Tabs, 2)
81 relayTab := h.tabs.ActiveTab()
82 relayPaneID := relayTab.Workspace.ActivePane().Pane.ID
83 assert.False(t, h.webview[relayPaneID].loadURICalled)
84
85 // The user moves on to another tab, then the app retries the same URL.
86 h.tabs.SetActive("tab-1")
87 *h.clock = h.clock.Add(30 * time.Second)
88
89 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
90
91 assert.Len(t, h.tabs.Tabs, 2, "no new tab must be created")
92 assert.Equal(t, relayTab.ID, h.tabs.ActiveTabID, "relay tab must be focused again")
93 assert.True(t, h.webview[relayPaneID].loadURICalled)
94 assert.Equal(t, relayTestURL, h.webview[relayPaneID].loadURILastURI)
95}
96
97func TestApp_OpenExternalURLCreatesNewPaneAfterReuseWindowExpires(t *testing.T) {
98 ctx := context.Background()
99 h := newRelayReuseHarness(t)
100
101 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
102 require.Len(t, h.tabs.Tabs, 2)
103
104 *h.clock = h.clock.Add(domainurl.RelayReuseWindow + time.Second)
105 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
106
107 assert.Len(t, h.tabs.Tabs, 3)
108}
109
110func TestApp_OpenExternalURLCreatesNewPaneForDifferentURL(t *testing.T) {
111 ctx := context.Background()
112 h := newRelayReuseHarness(t)
113
114 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
115 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL+"2"))
116
117 assert.Len(t, h.tabs.Tabs, 3)
118}
119
120func TestApp_OpenExternalURLCreatesNewPaneWhenRelayPaneWasClosed(t *testing.T) {
121 ctx := context.Background()
122 h := newRelayReuseHarness(t)
123
124 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
125 relayTab := h.tabs.ActiveTab()
126 h.tabs.Remove(relayTab.ID)
127 require.Len(t, h.tabs.Tabs, 1)
128
129 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
130
131 assert.Len(t, h.tabs.Tabs, 2)
132 assert.NotEqual(t, relayTab.ID, h.tabs.ActiveTabID)
133 assert.Equal(t, 1, h.app.externalRelayPanes.Len(), "stale entry replaced by the new pane")
134}
135
136func TestApp_OpenExternalURLCreatesNewPaneWhenRelayPaneCannotNavigate(t *testing.T) {
137 ctx := context.Background()
138 h := newRelayReuseHarness(t)
139
140 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
141 relayPaneID := h.tabs.ActiveTab().Workspace.ActivePane().Pane.ID
142 // The pane still exists but its WebView is gone, so navigation fails.
143 h.app.contentCoord.ReleaseWebView(ctx, relayPaneID)
144
145 require.NoError(t, h.app.OpenExternalURL(ctx, relayTestURL))
146
147 assert.Len(t, h.tabs.Tabs, 3)
148 assert.Equal(t, 1, h.app.externalRelayPanes.Len(), "failed entry replaced by the new pane")
149}