fix: keep fresh-window tabs and relay handoff responsive
- commit
- 9488c517dfd931c353550208f72cbb0c72797d1e
- author
- brice · · 2026-04-14 16:52 UTC
- parent
- 60b8e93
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +210 -29
M internal/infrastructure/desktop/browser_launch_relay.go +9 -0
| @@ -87,6 +87,9 @@ func (r *browserLaunchRelay) DeliverOpenFreshWindow(ctx context.Context, url str | |||
| 87 | 87 | if ctxErr := ctx.Err(); ctxErr != nil { | |
| 88 | 88 | return false, ctxErr | |
| 89 | 89 | } | |
| 90 | + | if _, ok := ctx.Deadline(); !ok { | |
| 91 | + | return false, decodeErr | |
| 92 | + | } | |
| 90 | 93 | if deadlineErr := setBrowserLaunchConnDeadline(ctx, conn); deadlineErr != nil { | |
| 91 | 94 | return false, deadlineErr | |
| 92 | 95 | } | |
| @@ -208,6 +211,9 @@ func (l *browserLaunchRelayListener) serve(ctx context.Context, opener port.Brow | |||
| 208 | 211 | ||
| 209 | 212 | func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *net.UnixConn, opener port.BrowserWindowOpener) { | |
| 210 | 213 | defer func() { _ = conn.Close() }() | |
| 214 | + | if err := conn.SetDeadline(time.Now().Add(browserLaunchIOTimeout)); err != nil { | |
| 215 | + | return | |
| 216 | + | } | |
| 211 | 217 | ||
| 212 | 218 | var request browserLaunchRequest | |
| 213 | 219 | if err := json.NewDecoder(conn).Decode(&request); err != nil { | |
| @@ -219,6 +225,9 @@ func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *n | |||
| 219 | 225 | response.Error = err.Error() | |
| 220 | 226 | } | |
| 221 | 227 | ||
| 228 | + | if err := conn.SetDeadline(time.Now().Add(browserLaunchIOTimeout)); err != nil { | |
| 229 | + | return | |
| 230 | + | } | |
| 222 | 231 | _ = json.NewEncoder(conn).Encode(response) | |
| 223 | 232 | } | |
| 224 | 233 | ||
| @@ -87,6 +87,9 @@ func (r *browserLaunchRelay) DeliverOpenFreshWindow(ctx context.Context, url str | |||||
| 87 | if ctxErr := ctx.Err(); ctxErr != nil { | 87 | if ctxErr := ctx.Err(); ctxErr != nil { | ||
| 88 | return false, ctxErr | 88 | return false, ctxErr | ||
| 89 | } | 89 | } | ||
| 90 | + | if _, ok := ctx.Deadline(); !ok { | |||
| 91 | + | return false, decodeErr | |||
| 92 | + | } | |||
| 90 | if deadlineErr := setBrowserLaunchConnDeadline(ctx, conn); deadlineErr != nil { | 93 | if deadlineErr := setBrowserLaunchConnDeadline(ctx, conn); deadlineErr != nil { | ||
| 91 | return false, deadlineErr | 94 | return false, deadlineErr | ||
| 92 | } | 95 | } | ||
| @@ -208,6 +211,9 @@ func (l *browserLaunchRelayListener) serve(ctx context.Context, opener port.Brow | |||||
| 208 | 211 | ||||
| 209 | func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *net.UnixConn, opener port.BrowserWindowOpener) { | 212 | func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *net.UnixConn, opener port.BrowserWindowOpener) { | ||
| 210 | defer func() { _ = conn.Close() }() | 213 | defer func() { _ = conn.Close() }() | ||
| 214 | + | if err := conn.SetDeadline(time.Now().Add(browserLaunchIOTimeout)); err != nil { | |||
| 215 | + | return | |||
| 216 | + | } | |||
| 211 | 217 | ||||
| 212 | var request browserLaunchRequest | 218 | var request browserLaunchRequest | ||
| 213 | if err := json.NewDecoder(conn).Decode(&request); err != nil { | 219 | if err := json.NewDecoder(conn).Decode(&request); err != nil { | ||
| @@ -219,6 +225,9 @@ func (*browserLaunchRelayListener) handleConnection(ctx context.Context, conn *n | |||||
| 219 | response.Error = err.Error() | 225 | response.Error = err.Error() | ||
| 220 | } | 226 | } | ||
| 221 | 227 | ||||
| 228 | + | if err := conn.SetDeadline(time.Now().Add(browserLaunchIOTimeout)); err != nil { | |||
| 229 | + | return | |||
| 230 | + | } | |||
| 222 | _ = json.NewEncoder(conn).Encode(response) | 231 | _ = json.NewEncoder(conn).Encode(response) | ||
| 223 | } | 232 | } | ||
| 224 | 233 | ||||
M internal/infrastructure/desktop/browser_launch_relay_test.go +94 -0
| @@ -245,3 +245,97 @@ func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RespectsResponseReadTimeout(t | |||
| 245 | 245 | require.ErrorIs(t, err, context.DeadlineExceeded) | |
| 246 | 246 | assert.False(t, delivered) | |
| 247 | 247 | } | |
| 248 | + | ||
| 249 | + | func TestBrowserLaunchRelay_DeliverOpenFreshWindow_ReturnsWithoutCallerDeadline(t *testing.T) { | |
| 250 | + | runtimeDir := shortTempDir(t) | |
| 251 | + | socketPath := filepath.Join(runtimeDir, "browser-launch.sock") | |
| 252 | + | require.NoError(t, os.MkdirAll(runtimeDir, 0o700)) | |
| 253 | + | ||
| 254 | + | listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) | |
| 255 | + | require.NoError(t, err) | |
| 256 | + | defer func() { | |
| 257 | + | _ = listener.Close() | |
| 258 | + | _ = os.Remove(socketPath) | |
| 259 | + | }() | |
| 260 | + | ||
| 261 | + | ready := make(chan struct{}) | |
| 262 | + | go func() { | |
| 263 | + | close(ready) | |
| 264 | + | conn, acceptErr := listener.AcceptUnix() | |
| 265 | + | if acceptErr != nil { | |
| 266 | + | return | |
| 267 | + | } | |
| 268 | + | defer conn.Close() | |
| 269 | + | <-time.After(time.Second) | |
| 270 | + | }() | |
| 271 | + | <-ready | |
| 272 | + | ||
| 273 | + | relay := NewBrowserLaunchRelay(testXDGPaths{runtimeDir: runtimeDir}) | |
| 274 | + | ||
| 275 | + | type deliverResult struct { | |
| 276 | + | delivered bool | |
| 277 | + | err error | |
| 278 | + | } | |
| 279 | + | result := make(chan deliverResult, 1) | |
| 280 | + | go func() { | |
| 281 | + | delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/slow") | |
| 282 | + | result <- deliverResult{delivered: delivered, err: err} | |
| 283 | + | }() | |
| 284 | + | ||
| 285 | + | select { | |
| 286 | + | case got := <-result: | |
| 287 | + | assert.False(t, got.delivered) | |
| 288 | + | require.Error(t, got.err) | |
| 289 | + | case <-time.After(300 * time.Millisecond): | |
| 290 | + | t.Fatal("DeliverOpenFreshWindow blocked without a caller deadline") | |
| 291 | + | } | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) { | |
| 295 | + | runtimeDir := filepath.Join(shortTempDir(t), "runtime") | |
| 296 | + | relay := NewBrowserLaunchRelay(testXDGPaths{runtimeDir: runtimeDir}) | |
| 297 | + | ||
| 298 | + | received := make(chan string, 1) | |
| 299 | + | ctx, cancel := context.WithCancel(context.Background()) | |
| 300 | + | defer cancel() | |
| 301 | + | ||
| 302 | + | closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error { | |
| 303 | + | received <- url | |
| 304 | + | return nil | |
| 305 | + | })) | |
| 306 | + | require.NoError(t, err) | |
| 307 | + | defer closer.Close() | |
| 308 | + | ||
| 309 | + | socketPath := filepath.Join(runtimeDir, "browser-launch.sock") | |
| 310 | + | waitForSocket(t, socketPath) | |
| 311 | + | ||
| 312 | + | conn, err := net.Dial("unix", socketPath) | |
| 313 | + | require.NoError(t, err) | |
| 314 | + | defer conn.Close() | |
| 315 | + | time.Sleep(20 * time.Millisecond) | |
| 316 | + | ||
| 317 | + | type deliverResult struct { | |
| 318 | + | delivered bool | |
| 319 | + | err error | |
| 320 | + | } | |
| 321 | + | result := make(chan deliverResult, 1) | |
| 322 | + | go func() { | |
| 323 | + | delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/recovered") | |
| 324 | + | result <- deliverResult{delivered: delivered, err: err} | |
| 325 | + | }() | |
| 326 | + | ||
| 327 | + | select { | |
| 328 | + | case got := <-result: | |
| 329 | + | require.NoError(t, got.err) | |
| 330 | + | require.True(t, got.delivered) | |
| 331 | + | case <-time.After(300 * time.Millisecond): | |
| 332 | + | t.Fatal("silent client stalled the relay listener") | |
| 333 | + | } | |
| 334 | + | ||
| 335 | + | select { | |
| 336 | + | case got := <-received: | |
| 337 | + | assert.Equal(t, "https://example.com/recovered", got) | |
| 338 | + | case <-time.After(time.Second): | |
| 339 | + | t.Fatal("expected opener to receive the URL") | |
| 340 | + | } | |
| 341 | + | } | |
| @@ -245,3 +245,97 @@ func TestBrowserLaunchRelay_DeliverOpenFreshWindow_RespectsResponseReadTimeout(t | |||||
| 245 | require.ErrorIs(t, err, context.DeadlineExceeded) | 245 | require.ErrorIs(t, err, context.DeadlineExceeded) | ||
| 246 | assert.False(t, delivered) | 246 | assert.False(t, delivered) | ||
| 247 | } | 247 | } | ||
| 248 | + | ||||
| 249 | + | func TestBrowserLaunchRelay_DeliverOpenFreshWindow_ReturnsWithoutCallerDeadline(t *testing.T) { | |||
| 250 | + | runtimeDir := shortTempDir(t) | |||
| 251 | + | socketPath := filepath.Join(runtimeDir, "browser-launch.sock") | |||
| 252 | + | require.NoError(t, os.MkdirAll(runtimeDir, 0o700)) | |||
| 253 | + | ||||
| 254 | + | listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"}) | |||
| 255 | + | require.NoError(t, err) | |||
| 256 | + | defer func() { | |||
| 257 | + | _ = listener.Close() | |||
| 258 | + | _ = os.Remove(socketPath) | |||
| 259 | + | }() | |||
| 260 | + | ||||
| 261 | + | ready := make(chan struct{}) | |||
| 262 | + | go func() { | |||
| 263 | + | close(ready) | |||
| 264 | + | conn, acceptErr := listener.AcceptUnix() | |||
| 265 | + | if acceptErr != nil { | |||
| 266 | + | return | |||
| 267 | + | } | |||
| 268 | + | defer conn.Close() | |||
| 269 | + | <-time.After(time.Second) | |||
| 270 | + | }() | |||
| 271 | + | <-ready | |||
| 272 | + | ||||
| 273 | + | relay := NewBrowserLaunchRelay(testXDGPaths{runtimeDir: runtimeDir}) | |||
| 274 | + | ||||
| 275 | + | type deliverResult struct { | |||
| 276 | + | delivered bool | |||
| 277 | + | err error | |||
| 278 | + | } | |||
| 279 | + | result := make(chan deliverResult, 1) | |||
| 280 | + | go func() { | |||
| 281 | + | delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/slow") | |||
| 282 | + | result <- deliverResult{delivered: delivered, err: err} | |||
| 283 | + | }() | |||
| 284 | + | ||||
| 285 | + | select { | |||
| 286 | + | case got := <-result: | |||
| 287 | + | assert.False(t, got.delivered) | |||
| 288 | + | require.Error(t, got.err) | |||
| 289 | + | case <-time.After(300 * time.Millisecond): | |||
| 290 | + | t.Fatal("DeliverOpenFreshWindow blocked without a caller deadline") | |||
| 291 | + | } | |||
| 292 | + | } | |||
| 293 | + | ||||
| 294 | + | func TestBrowserLaunchRelay_SilentClientDoesNotStallListener(t *testing.T) { | |||
| 295 | + | runtimeDir := filepath.Join(shortTempDir(t), "runtime") | |||
| 296 | + | relay := NewBrowserLaunchRelay(testXDGPaths{runtimeDir: runtimeDir}) | |||
| 297 | + | ||||
| 298 | + | received := make(chan string, 1) | |||
| 299 | + | ctx, cancel := context.WithCancel(context.Background()) | |||
| 300 | + | defer cancel() | |||
| 301 | + | ||||
| 302 | + | closer, err := relay.Listen(ctx, browserWindowOpenerFunc(func(_ context.Context, url string) error { | |||
| 303 | + | received <- url | |||
| 304 | + | return nil | |||
| 305 | + | })) | |||
| 306 | + | require.NoError(t, err) | |||
| 307 | + | defer closer.Close() | |||
| 308 | + | ||||
| 309 | + | socketPath := filepath.Join(runtimeDir, "browser-launch.sock") | |||
| 310 | + | waitForSocket(t, socketPath) | |||
| 311 | + | ||||
| 312 | + | conn, err := net.Dial("unix", socketPath) | |||
| 313 | + | require.NoError(t, err) | |||
| 314 | + | defer conn.Close() | |||
| 315 | + | time.Sleep(20 * time.Millisecond) | |||
| 316 | + | ||||
| 317 | + | type deliverResult struct { | |||
| 318 | + | delivered bool | |||
| 319 | + | err error | |||
| 320 | + | } | |||
| 321 | + | result := make(chan deliverResult, 1) | |||
| 322 | + | go func() { | |||
| 323 | + | delivered, err := relay.DeliverOpenFreshWindow(context.Background(), "https://example.com/recovered") | |||
| 324 | + | result <- deliverResult{delivered: delivered, err: err} | |||
| 325 | + | }() | |||
| 326 | + | ||||
| 327 | + | select { | |||
| 328 | + | case got := <-result: | |||
| 329 | + | require.NoError(t, got.err) | |||
| 330 | + | require.True(t, got.delivered) | |||
| 331 | + | case <-time.After(300 * time.Millisecond): | |||
| 332 | + | t.Fatal("silent client stalled the relay listener") | |||
| 333 | + | } | |||
| 334 | + | ||||
| 335 | + | select { | |||
| 336 | + | case got := <-received: | |||
| 337 | + | assert.Equal(t, "https://example.com/recovered", got) | |||
| 338 | + | case <-time.After(time.Second): | |||
| 339 | + | t.Fatal("expected opener to receive the URL") | |||
| 340 | + | } | |||
| 341 | + | } | |||
M internal/ui/app.go +48 -29
| @@ -529,44 +529,63 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||
| 529 | 529 | } | |
| 530 | 530 | ||
| 531 | 531 | if a.tabCoord != nil { | |
| 532 | - | a.tabCreationWindow = created | |
| 533 | - | defer func() { | |
| 534 | - | a.tabCreationWindow = nil | |
| 535 | - | }() | |
| 532 | + | return a.openFreshWindowWithTabCoord(ctx, url, created) | |
| 533 | + | } | |
| 536 | 534 | ||
| 537 | - | createdTab, createErr := a.tabCoord.Create(ctx, url) | |
| 538 | - | if createErr != nil { | |
| 539 | - | a.removeBrowserWindow(created.id) | |
| 540 | - | if created.mainWindow != nil { | |
| 541 | - | created.mainWindow.Destroy() | |
| 542 | - | } | |
| 543 | - | return createErr | |
| 535 | + | if a.tabsUC == nil { | |
| 536 | + | return nil | |
| 537 | + | } | |
| 538 | + | ||
| 539 | + | return a.openFreshWindowWithTabsUC(ctx, url, created) | |
| 540 | + | } | |
| 541 | + | ||
| 542 | + | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | |
| 543 | + | previousMainWindow := a.mainWindow | |
| 544 | + | if created.mainWindow != nil { | |
| 545 | + | a.tabCoord.SetMainWindow(created.mainWindow) | |
| 546 | + | } | |
| 547 | + | a.tabCreationWindow = created | |
| 548 | + | defer func() { | |
| 549 | + | a.tabCreationWindow = nil | |
| 550 | + | a.tabCoord.SetMainWindow(previousMainWindow) | |
| 551 | + | }() | |
| 552 | + | ||
| 553 | + | createdTab, createErr := a.tabCoord.Create(ctx, url) | |
| 554 | + | if createErr != nil { | |
| 555 | + | a.removeBrowserWindow(created.id) | |
| 556 | + | if created.mainWindow != nil { | |
| 557 | + | created.mainWindow.Destroy() | |
| 544 | 558 | } | |
| 545 | - | if a.workspaceViews[createdTab.ID] == nil { | |
| 546 | - | if a.tabs != nil { | |
| 547 | - | a.tabs.Remove(createdTab.ID) | |
| 548 | - | } | |
| 549 | - | if a.mainWindow != nil && a.mainWindow.TabBar() != nil { | |
| 550 | - | a.mainWindow.TabBar().RemoveTab(createdTab.ID) | |
| 551 | - | a.mainWindow.TabBar().SetActive(a.tabs.ActiveTabID) | |
| 552 | - | } | |
| 553 | - | a.removeBrowserWindow(created.id) | |
| 554 | - | if created.mainWindow != nil { | |
| 555 | - | created.mainWindow.Destroy() | |
| 559 | + | return createErr | |
| 560 | + | } | |
| 561 | + | if a.workspaceViews[createdTab.ID] == nil { | |
| 562 | + | if a.tabs != nil { | |
| 563 | + | a.tabs.Remove(createdTab.ID) | |
| 564 | + | } | |
| 565 | + | tabBar := a.mainWindow.TabBar() | |
| 566 | + | if created.mainWindow != nil && a.tabCoord != nil { | |
| 567 | + | if targetTabBar := a.tabCoord.GetTabBar(); targetTabBar != nil { | |
| 568 | + | tabBar = targetTabBar | |
| 556 | 569 | } | |
| 557 | - | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |
| 558 | 570 | } | |
| 559 | - | a.setBrowserWindowForTab(createdTab.ID, created) | |
| 571 | + | if tabBar != nil { | |
| 572 | + | tabBar.RemoveTab(createdTab.ID) | |
| 573 | + | tabBar.SetActive(a.tabs.ActiveTabID) | |
| 574 | + | } | |
| 575 | + | a.removeBrowserWindow(created.id) | |
| 560 | 576 | if created.mainWindow != nil { | |
| 561 | - | created.mainWindow.Show() | |
| 577 | + | created.mainWindow.Destroy() | |
| 562 | 578 | } | |
| 563 | - | return nil | |
| 579 | + | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |
| 564 | 580 | } | |
| 565 | - | ||
| 566 | - | if a.tabsUC == nil { | |
| 567 | - | return nil | |
| 581 | + | a.setBrowserWindowForTab(createdTab.ID, created) | |
| 582 | + | if created.mainWindow != nil { | |
| 583 | + | created.mainWindow.Show() | |
| 568 | 584 | } | |
| 585 | + | return nil | |
| 586 | + | } | |
| 569 | 587 | ||
| 588 | + | func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error { | |
| 570 | 589 | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | |
| 571 | 590 | TabList: a.tabs, | |
| 572 | 591 | InitialURL: url, | |
| @@ -529,44 +529,63 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||||
| 529 | } | 529 | } | ||
| 530 | 530 | ||||
| 531 | if a.tabCoord != nil { | 531 | if a.tabCoord != nil { | ||
| 532 | - | a.tabCreationWindow = created | 532 | + | return a.openFreshWindowWithTabCoord(ctx, url, created) |
| 533 | - | defer func() { | 533 | + | } |
| 534 | - | a.tabCreationWindow = nil | |||
| 535 | - | }() | |||
| 536 | 534 | ||||
| 537 | - | createdTab, createErr := a.tabCoord.Create(ctx, url) | 535 | + | if a.tabsUC == nil { |
| 538 | - | if createErr != nil { | 536 | + | return nil |
| 539 | - | a.removeBrowserWindow(created.id) | 537 | + | } |
| 540 | - | if created.mainWindow != nil { | 538 | + | |
| 541 | - | created.mainWindow.Destroy() | 539 | + | return a.openFreshWindowWithTabsUC(ctx, url, created) |
| 542 | - | } | 540 | + | } |
| 543 | - | return createErr | 541 | + | |
| 542 | + | func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error { | |||
| 543 | + | previousMainWindow := a.mainWindow | |||
| 544 | + | if created.mainWindow != nil { | |||
| 545 | + | a.tabCoord.SetMainWindow(created.mainWindow) | |||
| 546 | + | } | |||
| 547 | + | a.tabCreationWindow = created | |||
| 548 | + | defer func() { | |||
| 549 | + | a.tabCreationWindow = nil | |||
| 550 | + | a.tabCoord.SetMainWindow(previousMainWindow) | |||
| 551 | + | }() | |||
| 552 | + | ||||
| 553 | + | createdTab, createErr := a.tabCoord.Create(ctx, url) | |||
| 554 | + | if createErr != nil { | |||
| 555 | + | a.removeBrowserWindow(created.id) | |||
| 556 | + | if created.mainWindow != nil { | |||
| 557 | + | created.mainWindow.Destroy() | |||
| 544 | } | 558 | } | ||
| 545 | - | if a.workspaceViews[createdTab.ID] == nil { | 559 | + | return createErr |
| 546 | - | if a.tabs != nil { | 560 | + | } |
| 547 | - | a.tabs.Remove(createdTab.ID) | 561 | + | if a.workspaceViews[createdTab.ID] == nil { |
| 548 | - | } | 562 | + | if a.tabs != nil { |
| 549 | - | if a.mainWindow != nil && a.mainWindow.TabBar() != nil { | 563 | + | a.tabs.Remove(createdTab.ID) |
| 550 | - | a.mainWindow.TabBar().RemoveTab(createdTab.ID) | 564 | + | } |
| 551 | - | a.mainWindow.TabBar().SetActive(a.tabs.ActiveTabID) | 565 | + | tabBar := a.mainWindow.TabBar() |
| 552 | - | } | 566 | + | if created.mainWindow != nil && a.tabCoord != nil { |
| 553 | - | a.removeBrowserWindow(created.id) | 567 | + | if targetTabBar := a.tabCoord.GetTabBar(); targetTabBar != nil { |
| 554 | - | if created.mainWindow != nil { | 568 | + | tabBar = targetTabBar |
| 555 | - | created.mainWindow.Destroy() | |||
| 556 | } | 569 | } | ||
| 557 | - | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) | |||
| 558 | } | 570 | } | ||
| 559 | - | a.setBrowserWindowForTab(createdTab.ID, created) | 571 | + | if tabBar != nil { |
| 572 | + | tabBar.RemoveTab(createdTab.ID) | |||
| 573 | + | tabBar.SetActive(a.tabs.ActiveTabID) | |||
| 574 | + | } | |||
| 575 | + | a.removeBrowserWindow(created.id) | |||
| 560 | if created.mainWindow != nil { | 576 | if created.mainWindow != nil { | ||
| 561 | - | created.mainWindow.Show() | 577 | + | created.mainWindow.Destroy() |
| 562 | } | 578 | } | ||
| 563 | - | return nil | 579 | + | return fmt.Errorf("workspace view not created for tab %s", createdTab.ID) |
| 564 | } | 580 | } | ||
| 565 | - | 581 | + | a.setBrowserWindowForTab(createdTab.ID, created) | |
| 566 | - | if a.tabsUC == nil { | 582 | + | if created.mainWindow != nil { |
| 567 | - | return nil | 583 | + | created.mainWindow.Show() |
| 568 | } | 584 | } | ||
| 585 | + | return nil | |||
| 586 | + | } | |||
| 569 | 587 | ||||
| 588 | + | func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created *browserWindow) error { | |||
| 570 | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | 589 | output, err := a.tabsUC.Create(ctx, usecase.CreateTabInput{ | ||
| 571 | TabList: a.tabs, | 590 | TabList: a.tabs, | ||
| 572 | InitialURL: url, | 591 | InitialURL: url, | ||
M internal/ui/app_browser_launch_test.go +59 -0
| @@ -13,6 +13,8 @@ import ( | |||
| 13 | 13 | "github.com/bnema/dumber/internal/ui/component" | |
| 14 | 14 | "github.com/bnema/dumber/internal/ui/coordinator" | |
| 15 | 15 | "github.com/bnema/dumber/internal/ui/window" | |
| 16 | + | "github.com/bnema/puregotk/v4/gio" | |
| 17 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 16 | 18 | ) | |
| 17 | 19 | ||
| 18 | 20 | type testBrowserLaunchRelay struct { | |
| @@ -191,3 +193,60 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | |||
| 191 | 193 | t.Fatalf("tab bar active tab = %q, want %q", got, existingTabID) | |
| 192 | 194 | } | |
| 193 | 195 | } | |
| 196 | + | ||
| 197 | + | func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) { | |
| 198 | + | EnsureAdwaitaInitialized() | |
| 199 | + | appID := AppID | |
| 200 | + | gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue) | |
| 201 | + | if gtkApp == nil { | |
| 202 | + | t.Fatal("gtk application creation failed") | |
| 203 | + | } | |
| 204 | + | defer gtkApp.Unref() | |
| 205 | + | ||
| 206 | + | existingTabID := entity.TabID("existing-tab") | |
| 207 | + | oldWindow, err := window.New(context.Background(), gtkApp, "top") | |
| 208 | + | if err != nil { | |
| 209 | + | t.Fatalf("old window creation failed: %v", err) | |
| 210 | + | } | |
| 211 | + | newWindow, err := window.New(context.Background(), gtkApp, "top") | |
| 212 | + | if err != nil { | |
| 213 | + | t.Fatalf("new window creation failed: %v", err) | |
| 214 | + | } | |
| 215 | + | tabs := entity.NewTabList() | |
| 216 | + | tabsUC := usecase.NewManageTabsUseCase(func() string { return "id-1" }) | |
| 217 | + | ||
| 218 | + | app := &App{ | |
| 219 | + | tabs: tabs, | |
| 220 | + | tabsUC: tabsUC, | |
| 221 | + | browserWindows: make(map[string]*browserWindow), | |
| 222 | + | browserWindowFactory: func(context.Context, string) (*browserWindow, error) { | |
| 223 | + | return &browserWindow{id: "window-1", mainWindow: newWindow}, nil | |
| 224 | + | }, | |
| 225 | + | mainWindow: oldWindow, | |
| 226 | + | tabCoord: coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{ | |
| 227 | + | TabsUC: tabsUC, | |
| 228 | + | Tabs: tabs, | |
| 229 | + | MainWindow: oldWindow, | |
| 230 | + | }), | |
| 231 | + | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | |
| 232 | + | } | |
| 233 | + | app.tabs.Add(entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) | |
| 234 | + | app.tabCoord.SetOnTabCreated(func(ctx context.Context, tab *entity.Tab) { | |
| 235 | + | app.workspaceViews[tab.ID] = &component.WorkspaceView{} | |
| 236 | + | }) | |
| 237 | + | ||
| 238 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { | |
| 239 | + | t.Fatalf("OpenFreshWindow returned error: %v", err) | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | createdTabID := app.tabs.ActiveTabID | |
| 243 | + | if createdTabID == "" { | |
| 244 | + | t.Fatalf("created tab id = %q, want non-empty", createdTabID) | |
| 245 | + | } | |
| 246 | + | if got := windowTabBarActiveID(t, newWindow); got != createdTabID { | |
| 247 | + | t.Fatalf("new window tab bar active tab = %q, want %q", got, createdTabID) | |
| 248 | + | } | |
| 249 | + | if got := windowTabBarActiveID(t, oldWindow); got != "" { | |
| 250 | + | t.Fatalf("old window tab bar active tab = %q, want empty", got) | |
| 251 | + | } | |
| 252 | + | } | |
| @@ -13,6 +13,8 @@ import ( | |||||
| 13 | "github.com/bnema/dumber/internal/ui/component" | 13 | "github.com/bnema/dumber/internal/ui/component" | ||
| 14 | "github.com/bnema/dumber/internal/ui/coordinator" | 14 | "github.com/bnema/dumber/internal/ui/coordinator" | ||
| 15 | "github.com/bnema/dumber/internal/ui/window" | 15 | "github.com/bnema/dumber/internal/ui/window" | ||
| 16 | + | "github.com/bnema/puregotk/v4/gio" | |||
| 17 | + | "github.com/bnema/puregotk/v4/gtk" | |||
| 16 | ) | 18 | ) | ||
| 17 | 19 | ||||
| 18 | type testBrowserLaunchRelay struct { | 20 | type testBrowserLaunchRelay struct { | ||
| @@ -191,3 +193,60 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | |||||
| 191 | t.Fatalf("tab bar active tab = %q, want %q", got, existingTabID) | 193 | t.Fatalf("tab bar active tab = %q, want %q", got, existingTabID) | ||
| 192 | } | 194 | } | ||
| 193 | } | 195 | } | ||
| 196 | + | ||||
| 197 | + | func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) { | |||
| 198 | + | EnsureAdwaitaInitialized() | |||
| 199 | + | appID := AppID | |||
| 200 | + | gtkApp := gtk.NewApplication(&appID, gio.GApplicationNonUniqueValue) | |||
| 201 | + | if gtkApp == nil { | |||
| 202 | + | t.Fatal("gtk application creation failed") | |||
| 203 | + | } | |||
| 204 | + | defer gtkApp.Unref() | |||
| 205 | + | ||||
| 206 | + | existingTabID := entity.TabID("existing-tab") | |||
| 207 | + | oldWindow, err := window.New(context.Background(), gtkApp, "top") | |||
| 208 | + | if err != nil { | |||
| 209 | + | t.Fatalf("old window creation failed: %v", err) | |||
| 210 | + | } | |||
| 211 | + | newWindow, err := window.New(context.Background(), gtkApp, "top") | |||
| 212 | + | if err != nil { | |||
| 213 | + | t.Fatalf("new window creation failed: %v", err) | |||
| 214 | + | } | |||
| 215 | + | tabs := entity.NewTabList() | |||
| 216 | + | tabsUC := usecase.NewManageTabsUseCase(func() string { return "id-1" }) | |||
| 217 | + | ||||
| 218 | + | app := &App{ | |||
| 219 | + | tabs: tabs, | |||
| 220 | + | tabsUC: tabsUC, | |||
| 221 | + | browserWindows: make(map[string]*browserWindow), | |||
| 222 | + | browserWindowFactory: func(context.Context, string) (*browserWindow, error) { | |||
| 223 | + | return &browserWindow{id: "window-1", mainWindow: newWindow}, nil | |||
| 224 | + | }, | |||
| 225 | + | mainWindow: oldWindow, | |||
| 226 | + | tabCoord: coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{ | |||
| 227 | + | TabsUC: tabsUC, | |||
| 228 | + | Tabs: tabs, | |||
| 229 | + | MainWindow: oldWindow, | |||
| 230 | + | }), | |||
| 231 | + | workspaceViews: make(map[entity.TabID]*component.WorkspaceView), | |||
| 232 | + | } | |||
| 233 | + | app.tabs.Add(entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) | |||
| 234 | + | app.tabCoord.SetOnTabCreated(func(ctx context.Context, tab *entity.Tab) { | |||
| 235 | + | app.workspaceViews[tab.ID] = &component.WorkspaceView{} | |||
| 236 | + | }) | |||
| 237 | + | ||||
| 238 | + | if err := app.OpenFreshWindow(context.Background(), "https://example.com"); err != nil { | |||
| 239 | + | t.Fatalf("OpenFreshWindow returned error: %v", err) | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | createdTabID := app.tabs.ActiveTabID | |||
| 243 | + | if createdTabID == "" { | |||
| 244 | + | t.Fatalf("created tab id = %q, want non-empty", createdTabID) | |||
| 245 | + | } | |||
| 246 | + | if got := windowTabBarActiveID(t, newWindow); got != createdTabID { | |||
| 247 | + | t.Fatalf("new window tab bar active tab = %q, want %q", got, createdTabID) | |||
| 248 | + | } | |||
| 249 | + | if got := windowTabBarActiveID(t, oldWindow); got != "" { | |||
| 250 | + | t.Fatalf("old window tab bar active tab = %q, want empty", got) | |||
| 251 | + | } | |||
| 252 | + | } | |||