fix: resync tab bar after fresh window rollback
- commit
- 1147e01b16522cb27a9d12a989360125657a4f6d
- author
- brice · · 2026-04-14 16:10 UTC
- parent
- 7ef5ca7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +40 -9
M internal/ui/app.go +3 -2
| @@ -546,8 +546,9 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||
| 546 | 546 | if a.tabs != nil { | |
| 547 | 547 | a.tabs.Remove(tab.ID) | |
| 548 | 548 | } | |
| 549 | - | if created.mainWindow != nil && created.mainWindow.TabBar() != nil { | |
| 550 | - | created.mainWindow.TabBar().RemoveTab(tab.ID) | |
| 549 | + | if a.mainWindow != nil && a.mainWindow.TabBar() != nil { | |
| 550 | + | a.mainWindow.TabBar().RemoveTab(tab.ID) | |
| 551 | + | a.mainWindow.TabBar().SetActive(a.tabs.ActiveTabID) | |
| 551 | 552 | } | |
| 552 | 553 | a.removeBrowserWindow(created.id) | |
| 553 | 554 | if created.mainWindow != nil { | |
| @@ -546,8 +546,9 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error { | |||||
| 546 | if a.tabs != nil { | 546 | if a.tabs != nil { | ||
| 547 | a.tabs.Remove(tab.ID) | 547 | a.tabs.Remove(tab.ID) | ||
| 548 | } | 548 | } | ||
| 549 | - | if created.mainWindow != nil && created.mainWindow.TabBar() != nil { | 549 | + | if a.mainWindow != nil && a.mainWindow.TabBar() != nil { |
| 550 | - | created.mainWindow.TabBar().RemoveTab(tab.ID) | 550 | + | a.mainWindow.TabBar().RemoveTab(tab.ID) |
| 551 | + | a.mainWindow.TabBar().SetActive(a.tabs.ActiveTabID) | |||
| 551 | } | 552 | } | ||
| 552 | a.removeBrowserWindow(created.id) | 553 | a.removeBrowserWindow(created.id) | ||
| 553 | if created.mainWindow != nil { | 554 | if created.mainWindow != nil { | ||
M internal/ui/app_browser_launch_test.go +37 -7
| @@ -5,10 +5,12 @@ import ( | |||
| 5 | 5 | "io" | |
| 6 | 6 | "reflect" | |
| 7 | 7 | "testing" | |
| 8 | + | "unsafe" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/bnema/dumber/internal/application/port" | |
| 10 | 11 | "github.com/bnema/dumber/internal/application/usecase" | |
| 11 | 12 | "github.com/bnema/dumber/internal/domain/entity" | |
| 13 | + | "github.com/bnema/dumber/internal/ui/component" | |
| 12 | 14 | "github.com/bnema/dumber/internal/ui/coordinator" | |
| 13 | 15 | "github.com/bnema/dumber/internal/ui/layout" | |
| 14 | 16 | "github.com/bnema/dumber/internal/ui/window" | |
| @@ -72,6 +74,25 @@ func tabCoordinatorMainWindowPtr(t *testing.T, tc *coordinator.TabCoordinator) u | |||
| 72 | 74 | return fv.Pointer() | |
| 73 | 75 | } | |
| 74 | 76 | ||
| 77 | + | func setWindowTabBar(t *testing.T, mw *window.MainWindow, tabBar *component.TabBar) { | |
| 78 | + | t.Helper() | |
| 79 | + | ||
| 80 | + | rv := reflect.ValueOf(mw).Elem() | |
| 81 | + | fv := rv.FieldByName("tabBar") | |
| 82 | + | if !fv.IsValid() { | |
| 83 | + | t.Fatalf("MainWindow missing tabBar field") | |
| 84 | + | } | |
| 85 | + | reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(tabBar)) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func windowTabBarActiveID(t *testing.T, mw *window.MainWindow) entity.TabID { | |
| 89 | + | t.Helper() | |
| 90 | + | if mw == nil || mw.TabBar() == nil { | |
| 91 | + | return "" | |
| 92 | + | } | |
| 93 | + | return mw.TabBar().ActiveTabID() | |
| 94 | + | } | |
| 95 | + | ||
| 75 | 96 | type fakeWidgetFactory struct{} | |
| 76 | 97 | ||
| 77 | 98 | func (fakeWidgetFactory) NewPaned(layout.Orientation) layout.PanedWidget { return nil } | |
| @@ -149,6 +170,12 @@ func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T | |||
| 149 | 170 | ||
| 150 | 171 | func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | |
| 151 | 172 | created := &browserWindow{id: "window-1"} | |
| 173 | + | originalWindow := &window.MainWindow{} | |
| 174 | + | tabBar := &component.TabBar{} | |
| 175 | + | setWindowTabBar(t, originalWindow, tabBar) | |
| 176 | + | existingTabID := entity.TabID("existing-tab") | |
| 177 | + | staleTabID := entity.TabID("stale-tab") | |
| 178 | + | tabBar.SetActive(staleTabID) | |
| 152 | 179 | app := &App{ | |
| 153 | 180 | tabs: entity.NewTabList(), | |
| 154 | 181 | tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }), | |
| @@ -156,14 +183,14 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | |||
| 156 | 183 | browserWindowFactory: func(context.Context, string) (*browserWindow, error) { | |
| 157 | 184 | return created, nil | |
| 158 | 185 | }, | |
| 159 | - | tabCoord: coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{ | |
| 160 | - | TabsUC: usecase.NewManageTabsUseCase(func() string { return "id-2" }), | |
| 161 | - | Tabs: nil, | |
| 162 | - | MainWindow: &window.MainWindow{}, | |
| 163 | - | }), | |
| 164 | - | widgetFactory: fakeWidgetFactory{}, | |
| 186 | + | mainWindow: originalWindow, | |
| 165 | 187 | } | |
| 166 | - | app.tabs.Add(entity.NewTab(entity.TabID("existing-tab"), entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) | |
| 188 | + | app.tabs.Add(entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) | |
| 189 | + | app.tabCoord = coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{ | |
| 190 | + | TabsUC: app.tabsUC, | |
| 191 | + | Tabs: app.tabs, | |
| 192 | + | MainWindow: &window.MainWindow{}, | |
| 193 | + | }) | |
| 167 | 194 | ||
| 168 | 195 | if err := app.OpenFreshWindow(context.Background(), "https://example.com/fail"); err == nil { | |
| 169 | 196 | t.Fatalf("OpenFreshWindow = nil error, want failure") | |
| @@ -174,4 +201,7 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | |||
| 174 | 201 | if got := windowForTabCount(t, app); got != 0 { | |
| 175 | 202 | t.Fatalf("windowForTab length = %d, want 0", got) | |
| 176 | 203 | } | |
| 204 | + | if got := windowTabBarActiveID(t, originalWindow); got != existingTabID { | |
| 205 | + | t.Fatalf("tab bar active tab = %q, want %q", got, existingTabID) | |
| 206 | + | } | |
| 177 | 207 | } | |
| @@ -5,10 +5,12 @@ import ( | |||||
| 5 | "io" | 5 | "io" | ||
| 6 | "reflect" | 6 | "reflect" | ||
| 7 | "testing" | 7 | "testing" | ||
| 8 | + | "unsafe" | |||
| 8 | 9 | ||||
| 9 | "github.com/bnema/dumber/internal/application/port" | 10 | "github.com/bnema/dumber/internal/application/port" | ||
| 10 | "github.com/bnema/dumber/internal/application/usecase" | 11 | "github.com/bnema/dumber/internal/application/usecase" | ||
| 11 | "github.com/bnema/dumber/internal/domain/entity" | 12 | "github.com/bnema/dumber/internal/domain/entity" | ||
| 13 | + | "github.com/bnema/dumber/internal/ui/component" | |||
| 12 | "github.com/bnema/dumber/internal/ui/coordinator" | 14 | "github.com/bnema/dumber/internal/ui/coordinator" | ||
| 13 | "github.com/bnema/dumber/internal/ui/layout" | 15 | "github.com/bnema/dumber/internal/ui/layout" | ||
| 14 | "github.com/bnema/dumber/internal/ui/window" | 16 | "github.com/bnema/dumber/internal/ui/window" | ||
| @@ -72,6 +74,25 @@ func tabCoordinatorMainWindowPtr(t *testing.T, tc *coordinator.TabCoordinator) u | |||||
| 72 | return fv.Pointer() | 74 | return fv.Pointer() | ||
| 73 | } | 75 | } | ||
| 74 | 76 | ||||
| 77 | + | func setWindowTabBar(t *testing.T, mw *window.MainWindow, tabBar *component.TabBar) { | |||
| 78 | + | t.Helper() | |||
| 79 | + | ||||
| 80 | + | rv := reflect.ValueOf(mw).Elem() | |||
| 81 | + | fv := rv.FieldByName("tabBar") | |||
| 82 | + | if !fv.IsValid() { | |||
| 83 | + | t.Fatalf("MainWindow missing tabBar field") | |||
| 84 | + | } | |||
| 85 | + | reflect.NewAt(fv.Type(), unsafe.Pointer(fv.UnsafeAddr())).Elem().Set(reflect.ValueOf(tabBar)) | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func windowTabBarActiveID(t *testing.T, mw *window.MainWindow) entity.TabID { | |||
| 89 | + | t.Helper() | |||
| 90 | + | if mw == nil || mw.TabBar() == nil { | |||
| 91 | + | return "" | |||
| 92 | + | } | |||
| 93 | + | return mw.TabBar().ActiveTabID() | |||
| 94 | + | } | |||
| 95 | + | ||||
| 75 | type fakeWidgetFactory struct{} | 96 | type fakeWidgetFactory struct{} | ||
| 76 | 97 | ||||
| 77 | func (fakeWidgetFactory) NewPaned(layout.Orientation) layout.PanedWidget { return nil } | 98 | func (fakeWidgetFactory) NewPaned(layout.Orientation) layout.PanedWidget { return nil } | ||
| @@ -149,6 +170,12 @@ func TestApp_RemoveBrowserWindowRebindsPromotedTabCoordinatorWindow(t *testing.T | |||||
| 149 | 170 | ||||
| 150 | func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | 171 | func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | ||
| 151 | created := &browserWindow{id: "window-1"} | 172 | created := &browserWindow{id: "window-1"} | ||
| 173 | + | originalWindow := &window.MainWindow{} | |||
| 174 | + | tabBar := &component.TabBar{} | |||
| 175 | + | setWindowTabBar(t, originalWindow, tabBar) | |||
| 176 | + | existingTabID := entity.TabID("existing-tab") | |||
| 177 | + | staleTabID := entity.TabID("stale-tab") | |||
| 178 | + | tabBar.SetActive(staleTabID) | |||
| 152 | app := &App{ | 179 | app := &App{ | ||
| 153 | tabs: entity.NewTabList(), | 180 | tabs: entity.NewTabList(), | ||
| 154 | tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }), | 181 | tabsUC: usecase.NewManageTabsUseCase(func() string { return "id-1" }), | ||
| @@ -156,14 +183,14 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | |||||
| 156 | browserWindowFactory: func(context.Context, string) (*browserWindow, error) { | 183 | browserWindowFactory: func(context.Context, string) (*browserWindow, error) { | ||
| 157 | return created, nil | 184 | return created, nil | ||
| 158 | }, | 185 | }, | ||
| 159 | - | tabCoord: coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{ | 186 | + | mainWindow: originalWindow, |
| 160 | - | TabsUC: usecase.NewManageTabsUseCase(func() string { return "id-2" }), | |||
| 161 | - | Tabs: nil, | |||
| 162 | - | MainWindow: &window.MainWindow{}, | |||
| 163 | - | }), | |||
| 164 | - | widgetFactory: fakeWidgetFactory{}, | |||
| 165 | } | 187 | } | ||
| 166 | - | app.tabs.Add(entity.NewTab(entity.TabID("existing-tab"), entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) | 188 | + | app.tabs.Add(entity.NewTab(existingTabID, entity.WorkspaceID("existing-workspace"), entity.NewPane(entity.PaneID("existing-pane")))) |
| 189 | + | app.tabCoord = coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{ | |||
| 190 | + | TabsUC: app.tabsUC, | |||
| 191 | + | Tabs: app.tabs, | |||
| 192 | + | MainWindow: &window.MainWindow{}, | |||
| 193 | + | }) | |||
| 167 | 194 | ||||
| 168 | if err := app.OpenFreshWindow(context.Background(), "https://example.com/fail"); err == nil { | 195 | if err := app.OpenFreshWindow(context.Background(), "https://example.com/fail"); err == nil { | ||
| 169 | t.Fatalf("OpenFreshWindow = nil error, want failure") | 196 | t.Fatalf("OpenFreshWindow = nil error, want failure") | ||
| @@ -174,4 +201,7 @@ func TestApp_OpenFreshWindowRollsBackOnTabCreationFailure(t *testing.T) { | |||||
| 174 | if got := windowForTabCount(t, app); got != 0 { | 201 | if got := windowForTabCount(t, app); got != 0 { | ||
| 175 | t.Fatalf("windowForTab length = %d, want 0", got) | 202 | t.Fatalf("windowForTab length = %d, want 0", got) | ||
| 176 | } | 203 | } | ||
| 204 | + | if got := windowTabBarActiveID(t, originalWindow); got != existingTabID { | |||
| 205 | + | t.Fatalf("tab bar active tab = %q, want %q", got, existingTabID) | |||
| 206 | + | } | |||
| 177 | } | 207 | } | ||