test(ui): cover floating browsing context routing
- commit
- 3eb91808852eb7b1dbdb0f52deb83d03b7eb4349
- author
- brice · · 2026-07-25 08:33 UTC
- committer
- brice · · 2026-07-26 16:42 UTC
- parent
- 1ee31e6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +89 -0
M docs/reference/floating-pane.md +6 -0
| @@ -16,6 +16,12 @@ The floating pane is an overlay workspace pane for quick-access pages like searc | |||
| 16 | 16 | - `Ctrl+W` on an active floating pane is a full close. It releases that floating session and its WebView. | |
| 17 | 17 | - After a floating pane is closed with `Ctrl+W`, opening it again starts from a fresh state. | |
| 18 | 18 | ||
| 19 | + | ### Links and popups | |
| 20 | + | ||
| 21 | + | New-page links, middle-clicks, and featureless `window.open()` calls from a floating pane open in a new Dumber window. OAuth flows and scripts that request popup dimensions or restricted browser chrome open in a related popup window so authentication and opener messaging continue to work. | |
| 22 | + | ||
| 23 | + | Detached Dumber windows remain open when the source floating pane is hidden or closed. | |
| 24 | + | ||
| 19 | 25 | ## Configuration | |
| 20 | 26 | ||
| 21 | 27 | | Key | Type | Default | Description | | |
| @@ -16,6 +16,12 @@ The floating pane is an overlay workspace pane for quick-access pages like searc | |||||
| 16 | - `Ctrl+W` on an active floating pane is a full close. It releases that floating session and its WebView. | 16 | - `Ctrl+W` on an active floating pane is a full close. It releases that floating session and its WebView. | ||
| 17 | - After a floating pane is closed with `Ctrl+W`, opening it again starts from a fresh state. | 17 | - After a floating pane is closed with `Ctrl+W`, opening it again starts from a fresh state. | ||
| 18 | 18 | ||||
| 19 | + | ### Links and popups | |||
| 20 | + | ||||
| 21 | + | New-page links, middle-clicks, and featureless `window.open()` calls from a floating pane open in a new Dumber window. OAuth flows and scripts that request popup dimensions or restricted browser chrome open in a related popup window so authentication and opener messaging continue to work. | |||
| 22 | + | ||||
| 23 | + | Detached Dumber windows remain open when the source floating pane is hidden or closed. | |||
| 24 | + | ||||
| 19 | ## Configuration | 25 | ## Configuration | ||
| 20 | 26 | ||||
| 21 | | Key | Type | Default | Description | | 27 | | Key | Type | Default | Description | | ||
M internal/ui/app_browser_launch_test.go +18 -0
| @@ -1313,6 +1313,24 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) { | |||
| 1313 | 1313 | } | |
| 1314 | 1314 | } | |
| 1315 | 1315 | ||
| 1316 | + | func TestApp_PopupOwnerWindowIDForFloatingPaneUsesSourceTabWindow(t *testing.T) { | |
| 1317 | + | tabID := entity.TabID("source-tab") | |
| 1318 | + | owner := &browserWindow{id: "owner-window", tabs: entity.NewTabList()} | |
| 1319 | + | paneID := entity.PaneID("floating-pane") | |
| 1320 | + | app := &App{ | |
| 1321 | + | browserWindows: map[string]*browserWindow{owner.id: owner}, | |
| 1322 | + | windowForTab: map[entity.TabID]*browserWindow{tabID: owner}, | |
| 1323 | + | floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{ | |
| 1324 | + | {tabID: tabID, sessionID: "profile"}: {paneID: paneID}, | |
| 1325 | + | }, | |
| 1326 | + | } | |
| 1327 | + | ||
| 1328 | + | windowID, ok := app.popupOwnerWindowIDForPane(paneID) | |
| 1329 | + | ||
| 1330 | + | require.True(t, ok) | |
| 1331 | + | require.Equal(t, owner.id, windowID) | |
| 1332 | + | } | |
| 1333 | + | ||
| 1316 | 1334 | func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) { | |
| 1317 | 1335 | oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}} | |
| 1318 | 1336 | newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}} | |
| @@ -1313,6 +1313,24 @@ func TestApp_RestoreSessionWiresStackedPaneTitleBarCallbacks(t *testing.T) { | |||||
| 1313 | } | 1313 | } | ||
| 1314 | } | 1314 | } | ||
| 1315 | 1315 | ||||
| 1316 | + | func TestApp_PopupOwnerWindowIDForFloatingPaneUsesSourceTabWindow(t *testing.T) { | |||
| 1317 | + | tabID := entity.TabID("source-tab") | |||
| 1318 | + | owner := &browserWindow{id: "owner-window", tabs: entity.NewTabList()} | |||
| 1319 | + | paneID := entity.PaneID("floating-pane") | |||
| 1320 | + | app := &App{ | |||
| 1321 | + | browserWindows: map[string]*browserWindow{owner.id: owner}, | |||
| 1322 | + | windowForTab: map[entity.TabID]*browserWindow{tabID: owner}, | |||
| 1323 | + | floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{ | |||
| 1324 | + | {tabID: tabID, sessionID: "profile"}: {paneID: paneID}, | |||
| 1325 | + | }, | |||
| 1326 | + | } | |||
| 1327 | + | ||||
| 1328 | + | windowID, ok := app.popupOwnerWindowIDForPane(paneID) | |||
| 1329 | + | ||||
| 1330 | + | require.True(t, ok) | |||
| 1331 | + | require.Equal(t, owner.id, windowID) | |||
| 1332 | + | } | |||
| 1333 | + | ||||
| 1316 | func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) { | 1334 | func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T) { | ||
| 1317 | oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}} | 1335 | oldWindow := &browserWindow{id: "window-1", mainWindow: &window.MainWindow{}} | ||
| 1318 | newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}} | 1336 | newWindow := &browserWindow{id: "window-2", mainWindow: &window.MainWindow{}} | ||
M internal/ui/browser_window.go +9 -0
| @@ -366,6 +366,7 @@ func (a *App) removeBrowserWindow(id string) { | |||
| 366 | 366 | wasMainWindow := removed != nil && removed.mainWindow == a.mainWindow | |
| 367 | 367 | ||
| 368 | 368 | a.removeBrowserWindowOrder(id) | |
| 369 | + | a.releaseNativePopupsForBrowserWindow(id) | |
| 369 | 370 | a.releaseBrowserWindowTabs(context.Background(), id, removed) | |
| 370 | 371 | if removed != nil { | |
| 371 | 372 | removed.teardownForDestroy() | |
| @@ -382,6 +383,14 @@ func (a *App) removeBrowserWindow(id string) { | |||
| 382 | 383 | } | |
| 383 | 384 | } | |
| 384 | 385 | ||
| 386 | + | func (a *App) releaseNativePopupsForBrowserWindow(windowID string) { | |
| 387 | + | for popupID, popup := range a.nativePopupWindows { | |
| 388 | + | if popup != nil && popup.parentWindowID == windowID { | |
| 389 | + | a.releaseNativePopupWindow(popupID, nativePopupReleaseDestroy) | |
| 390 | + | } | |
| 391 | + | } | |
| 392 | + | } | |
| 393 | + | ||
| 385 | 394 | func (a *App) removeBrowserWindowOrder(id string) { | |
| 386 | 395 | for i, wid := range a.browserWindowOrder { | |
| 387 | 396 | if wid == id { | |
| @@ -366,6 +366,7 @@ func (a *App) removeBrowserWindow(id string) { | |||||
| 366 | wasMainWindow := removed != nil && removed.mainWindow == a.mainWindow | 366 | wasMainWindow := removed != nil && removed.mainWindow == a.mainWindow | ||
| 367 | 367 | ||||
| 368 | a.removeBrowserWindowOrder(id) | 368 | a.removeBrowserWindowOrder(id) | ||
| 369 | + | a.releaseNativePopupsForBrowserWindow(id) | |||
| 369 | a.releaseBrowserWindowTabs(context.Background(), id, removed) | 370 | a.releaseBrowserWindowTabs(context.Background(), id, removed) | ||
| 370 | if removed != nil { | 371 | if removed != nil { | ||
| 371 | removed.teardownForDestroy() | 372 | removed.teardownForDestroy() | ||
| @@ -382,6 +383,14 @@ func (a *App) removeBrowserWindow(id string) { | |||||
| 382 | } | 383 | } | ||
| 383 | } | 384 | } | ||
| 384 | 385 | ||||
| 386 | + | func (a *App) releaseNativePopupsForBrowserWindow(windowID string) { | |||
| 387 | + | for popupID, popup := range a.nativePopupWindows { | |||
| 388 | + | if popup != nil && popup.parentWindowID == windowID { | |||
| 389 | + | a.releaseNativePopupWindow(popupID, nativePopupReleaseDestroy) | |||
| 390 | + | } | |||
| 391 | + | } | |||
| 392 | + | } | |||
| 393 | + | ||||
| 385 | func (a *App) removeBrowserWindowOrder(id string) { | 394 | func (a *App) removeBrowserWindowOrder(id string) { | ||
| 386 | for i, wid := range a.browserWindowOrder { | 395 | for i, wid := range a.browserWindowOrder { | ||
| 387 | if wid == id { | 396 | if wid == id { | ||
M internal/ui/browser_window_test.go +26 -0
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "unsafe" | |
| 9 | 9 | ||
| 10 | 10 | "github.com/bnema/dumber/internal/application/port" | |
| 11 | + | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | |
| 11 | 12 | "github.com/bnema/dumber/internal/application/usecase" | |
| 12 | 13 | "github.com/bnema/dumber/internal/domain/entity" | |
| 13 | 14 | "github.com/bnema/dumber/internal/infrastructure/config" | |
| @@ -274,6 +275,31 @@ func TestBrowserWindow_RemoveBrowserWindowReleasesOwnedTabWorkspaceWebViews(t *t | |||
| 274 | 275 | assert.Same(t, otherWV, contentCoord.GetWebView(otherTab.Workspace.ActivePaneID)) | |
| 275 | 276 | } | |
| 276 | 277 | ||
| 278 | + | func TestBrowserWindow_RemoveBrowserWindowReleasesOnlyRelatedNativePopups(t *testing.T) { | |
| 279 | + | closedWebView := portmocks.NewMockWebView(t) | |
| 280 | + | closedWebView.EXPECT().IsDestroyed().Return(false).Once() | |
| 281 | + | closedWebView.EXPECT().Destroy().Once() | |
| 282 | + | unrelatedWebView := portmocks.NewMockWebView(t) | |
| 283 | + | closedShell := &popupDestroySpy{} | |
| 284 | + | unrelatedShell := &popupDestroySpy{} | |
| 285 | + | removed := &browserWindow{id: "owner-window", tabs: entity.NewTabList()} | |
| 286 | + | remaining := &browserWindow{id: "other-window", tabs: entity.NewTabList()} | |
| 287 | + | app := &App{ | |
| 288 | + | browserWindows: map[string]*browserWindow{removed.id: removed, remaining.id: remaining}, | |
| 289 | + | nativePopupWindows: map[port.WebViewID]*nativePopupWindow{ | |
| 290 | + | 1: {popupID: 1, parentWindowID: removed.id, webView: closedWebView, popupWindow: closedShell}, | |
| 291 | + | 2: {popupID: 2, parentWindowID: remaining.id, webView: unrelatedWebView, popupWindow: unrelatedShell}, | |
| 292 | + | }, | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | app.removeBrowserWindow(removed.id) | |
| 296 | + | ||
| 297 | + | assert.NotContains(t, app.nativePopupWindows, port.WebViewID(1)) | |
| 298 | + | assert.Contains(t, app.nativePopupWindows, port.WebViewID(2)) | |
| 299 | + | assert.Equal(t, 1, closedShell.destroyed) | |
| 300 | + | assert.Zero(t, unrelatedShell.destroyed) | |
| 301 | + | } | |
| 302 | + | ||
| 277 | 303 | func TestBrowserWindow_RemoveBrowserWindowCleansOwnedTabState(t *testing.T) { | |
| 278 | 304 | ownedTab := entity.NewTab(entity.TabID("tab-owned"), entity.WorkspaceID("workspace-owned"), entity.NewPane(entity.PaneID("pane-owned"))) | |
| 279 | 305 | otherTab := entity.NewTab(entity.TabID("tab-other"), entity.WorkspaceID("workspace-other"), entity.NewPane(entity.PaneID("pane-other"))) | |
| @@ -8,6 +8,7 @@ import ( | |||||
| 8 | "unsafe" | 8 | "unsafe" | ||
| 9 | 9 | ||||
| 10 | "github.com/bnema/dumber/internal/application/port" | 10 | "github.com/bnema/dumber/internal/application/port" | ||
| 11 | + | portmocks "github.com/bnema/dumber/internal/application/port/mocks" | |||
| 11 | "github.com/bnema/dumber/internal/application/usecase" | 12 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 12 | "github.com/bnema/dumber/internal/domain/entity" | 13 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 13 | "github.com/bnema/dumber/internal/infrastructure/config" | 14 | "github.com/bnema/dumber/internal/infrastructure/config" | ||
| @@ -274,6 +275,31 @@ func TestBrowserWindow_RemoveBrowserWindowReleasesOwnedTabWorkspaceWebViews(t *t | |||||
| 274 | assert.Same(t, otherWV, contentCoord.GetWebView(otherTab.Workspace.ActivePaneID)) | 275 | assert.Same(t, otherWV, contentCoord.GetWebView(otherTab.Workspace.ActivePaneID)) | ||
| 275 | } | 276 | } | ||
| 276 | 277 | ||||
| 278 | + | func TestBrowserWindow_RemoveBrowserWindowReleasesOnlyRelatedNativePopups(t *testing.T) { | |||
| 279 | + | closedWebView := portmocks.NewMockWebView(t) | |||
| 280 | + | closedWebView.EXPECT().IsDestroyed().Return(false).Once() | |||
| 281 | + | closedWebView.EXPECT().Destroy().Once() | |||
| 282 | + | unrelatedWebView := portmocks.NewMockWebView(t) | |||
| 283 | + | closedShell := &popupDestroySpy{} | |||
| 284 | + | unrelatedShell := &popupDestroySpy{} | |||
| 285 | + | removed := &browserWindow{id: "owner-window", tabs: entity.NewTabList()} | |||
| 286 | + | remaining := &browserWindow{id: "other-window", tabs: entity.NewTabList()} | |||
| 287 | + | app := &App{ | |||
| 288 | + | browserWindows: map[string]*browserWindow{removed.id: removed, remaining.id: remaining}, | |||
| 289 | + | nativePopupWindows: map[port.WebViewID]*nativePopupWindow{ | |||
| 290 | + | 1: {popupID: 1, parentWindowID: removed.id, webView: closedWebView, popupWindow: closedShell}, | |||
| 291 | + | 2: {popupID: 2, parentWindowID: remaining.id, webView: unrelatedWebView, popupWindow: unrelatedShell}, | |||
| 292 | + | }, | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | app.removeBrowserWindow(removed.id) | |||
| 296 | + | ||||
| 297 | + | assert.NotContains(t, app.nativePopupWindows, port.WebViewID(1)) | |||
| 298 | + | assert.Contains(t, app.nativePopupWindows, port.WebViewID(2)) | |||
| 299 | + | assert.Equal(t, 1, closedShell.destroyed) | |||
| 300 | + | assert.Zero(t, unrelatedShell.destroyed) | |||
| 301 | + | } | |||
| 302 | + | ||||
| 277 | func TestBrowserWindow_RemoveBrowserWindowCleansOwnedTabState(t *testing.T) { | 303 | func TestBrowserWindow_RemoveBrowserWindowCleansOwnedTabState(t *testing.T) { | ||
| 278 | ownedTab := entity.NewTab(entity.TabID("tab-owned"), entity.WorkspaceID("workspace-owned"), entity.NewPane(entity.PaneID("pane-owned"))) | 304 | ownedTab := entity.NewTab(entity.TabID("tab-owned"), entity.WorkspaceID("workspace-owned"), entity.NewPane(entity.PaneID("pane-owned"))) | ||
| 279 | otherTab := entity.NewTab(entity.TabID("tab-other"), entity.WorkspaceID("workspace-other"), entity.NewPane(entity.PaneID("pane-other"))) | 305 | otherTab := entity.NewTab(entity.TabID("tab-other"), entity.WorkspaceID("workspace-other"), entity.NewPane(entity.PaneID("pane-other"))) | ||
M internal/ui/floating_pane_test.go +30 -0
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | "github.com/bnema/dumber/internal/domain/entity" | |
| 9 | 9 | "github.com/bnema/dumber/internal/ui/component" | |
| 10 | + | contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content" | |
| 10 | 11 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | |
| 11 | 12 | "github.com/stretchr/testify/assert" | |
| 12 | 13 | "github.com/stretchr/testify/require" | |
| @@ -312,6 +313,35 @@ func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) { | |||
| 312 | 313 | assert.False(t, handled) | |
| 313 | 314 | } | |
| 314 | 315 | ||
| 316 | + | func TestFloatingPane_HideAndCloseLeaveDetachedBrowserWindowAlive(t *testing.T) { | |
| 317 | + | ctx := context.Background() | |
| 318 | + | app, tabID, session := newFloatingPaneTestApp(t) | |
| 319 | + | pool := &recordingWebViewPool{} | |
| 320 | + | contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil) | |
| 321 | + | sourceWebView := &recordingWebView{id: 301} | |
| 322 | + | detachedWebView := &recordingWebView{id: 302} | |
| 323 | + | detachedPaneID := entity.PaneID("detached-pane") | |
| 324 | + | contentCoord.RegisterPopupWebView(session.paneID, sourceWebView) | |
| 325 | + | contentCoord.RegisterPopupWebView(detachedPaneID, detachedWebView) | |
| 326 | + | app.contentCoord = contentCoord | |
| 327 | + | detachedWindow := &browserWindow{id: "detached-window", tabs: entity.NewTabList()} | |
| 328 | + | app.browserWindows[detachedWindow.id] = detachedWindow | |
| 329 | + | ||
| 330 | + | require.NoError(t, session.pane.ShowURL(ctx, "https://example.com/source")) | |
| 331 | + | app.hideFloatingSession(ctx, session) | |
| 332 | + | assert.Same(t, detachedWebView, contentCoord.GetWebView(detachedPaneID)) | |
| 333 | + | assert.Same(t, detachedWindow, app.browserWindows[detachedWindow.id]) | |
| 334 | + | ||
| 335 | + | key := floatingSessionKey{tabID: tabID, sessionID: floatingSessionIDDefault} | |
| 336 | + | app.releaseFloatingSession(ctx, key, session) | |
| 337 | + | ||
| 338 | + | assert.Same(t, detachedWebView, contentCoord.GetWebView(detachedPaneID)) | |
| 339 | + | assert.Same(t, detachedWindow, app.browserWindows[detachedWindow.id]) | |
| 340 | + | assert.Zero(t, detachedWebView.destroyCalls) | |
| 341 | + | require.Len(t, pool.released, 1) | |
| 342 | + | assert.Same(t, sourceWebView, pool.released[0]) | |
| 343 | + | } | |
| 344 | + | ||
| 315 | 345 | func TestFloatingPane_HandleGlobalEscape_HidesVisibleFloatingPane(t *testing.T) { | |
| 316 | 346 | app, _, session := newFloatingPaneTestApp(t) | |
| 317 | 347 | ||
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | "github.com/bnema/dumber/internal/domain/entity" | 8 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 9 | "github.com/bnema/dumber/internal/ui/component" | 9 | "github.com/bnema/dumber/internal/ui/component" | ||
| 10 | + | contentcoord "github.com/bnema/dumber/internal/ui/coordinator/content" | |||
| 10 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | 11 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | ||
| 11 | "github.com/stretchr/testify/assert" | 12 | "github.com/stretchr/testify/assert" | ||
| 12 | "github.com/stretchr/testify/require" | 13 | "github.com/stretchr/testify/require" | ||
| @@ -312,6 +313,35 @@ func TestFloatingPane_CloseActiveFloatingSession(t *testing.T) { | |||||
| 312 | assert.False(t, handled) | 313 | assert.False(t, handled) | ||
| 313 | } | 314 | } | ||
| 314 | 315 | ||||
| 316 | + | func TestFloatingPane_HideAndCloseLeaveDetachedBrowserWindowAlive(t *testing.T) { | |||
| 317 | + | ctx := context.Background() | |||
| 318 | + | app, tabID, session := newFloatingPaneTestApp(t) | |||
| 319 | + | pool := &recordingWebViewPool{} | |||
| 320 | + | contentCoord := contentcoord.NewCoordinator(ctx, pool, nil, nil, nil, nil, nil, nil) | |||
| 321 | + | sourceWebView := &recordingWebView{id: 301} | |||
| 322 | + | detachedWebView := &recordingWebView{id: 302} | |||
| 323 | + | detachedPaneID := entity.PaneID("detached-pane") | |||
| 324 | + | contentCoord.RegisterPopupWebView(session.paneID, sourceWebView) | |||
| 325 | + | contentCoord.RegisterPopupWebView(detachedPaneID, detachedWebView) | |||
| 326 | + | app.contentCoord = contentCoord | |||
| 327 | + | detachedWindow := &browserWindow{id: "detached-window", tabs: entity.NewTabList()} | |||
| 328 | + | app.browserWindows[detachedWindow.id] = detachedWindow | |||
| 329 | + | ||||
| 330 | + | require.NoError(t, session.pane.ShowURL(ctx, "https://example.com/source")) | |||
| 331 | + | app.hideFloatingSession(ctx, session) | |||
| 332 | + | assert.Same(t, detachedWebView, contentCoord.GetWebView(detachedPaneID)) | |||
| 333 | + | assert.Same(t, detachedWindow, app.browserWindows[detachedWindow.id]) | |||
| 334 | + | ||||
| 335 | + | key := floatingSessionKey{tabID: tabID, sessionID: floatingSessionIDDefault} | |||
| 336 | + | app.releaseFloatingSession(ctx, key, session) | |||
| 337 | + | ||||
| 338 | + | assert.Same(t, detachedWebView, contentCoord.GetWebView(detachedPaneID)) | |||
| 339 | + | assert.Same(t, detachedWindow, app.browserWindows[detachedWindow.id]) | |||
| 340 | + | assert.Zero(t, detachedWebView.destroyCalls) | |||
| 341 | + | require.Len(t, pool.released, 1) | |||
| 342 | + | assert.Same(t, sourceWebView, pool.released[0]) | |||
| 343 | + | } | |||
| 344 | + | ||||
| 315 | func TestFloatingPane_HandleGlobalEscape_HidesVisibleFloatingPane(t *testing.T) { | 345 | func TestFloatingPane_HandleGlobalEscape_HidesVisibleFloatingPane(t *testing.T) { | ||
| 316 | app, _, session := newFloatingPaneTestApp(t) | 346 | app, _, session := newFloatingPaneTestApp(t) | ||
| 317 | 347 | ||||