test(ui): clean up deferred toaster callbacks
- commit
- 0600fe7b718b63b6aba757ab7222381891df81b2
- author
- brice · · 2026-08-12 08:58 UTC
- parent
- 2548d05
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +35 -1
M internal/ui/app_browser_launch_test.go +23 -1
| @@ -35,7 +35,9 @@ import ( | |||
| 35 | 35 | "github.com/bnema/dumber/internal/ui/window" | |
| 36 | 36 | "github.com/bnema/puregotk/v4/gdk" | |
| 37 | 37 | "github.com/bnema/puregotk/v4/gio" | |
| 38 | + | "github.com/bnema/puregotk/v4/glib" | |
| 38 | 39 | "github.com/bnema/puregotk/v4/gtk" | |
| 40 | + | "github.com/stretchr/testify/assert" | |
| 39 | 41 | "github.com/stretchr/testify/mock" | |
| 40 | 42 | "github.com/stretchr/testify/require" | |
| 41 | 43 | ) | |
| @@ -979,7 +981,9 @@ func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBox | |||
| 979 | 981 | label.EXPECT().SetCanTarget(false).Once() | |
| 980 | 982 | label.EXPECT().SetCanFocus(false).Once() | |
| 981 | 983 | ||
| 982 | - | return component.NewToaster(factory), box, label | |
| 984 | + | toaster := component.NewToaster(factory) | |
| 985 | + | t.Cleanup(toaster.CancelAutoDismiss) | |
| 986 | + | return toaster, box, label | |
| 983 | 987 | } | |
| 984 | 988 | ||
| 985 | 989 | func TestApp_ShowFilterStatusUsesLastFocusedBrowserWindowToaster(t *testing.T) { | |
| @@ -997,6 +1001,24 @@ func TestApp_ShowFilterStatusUsesLastFocusedBrowserWindowToaster(t *testing.T) { | |||
| 997 | 1001 | app.showFilterStatus(ctx, port.FilterStatus{State: port.FilterStateLoading, Message: "Ad blocker loading"}) | |
| 998 | 1002 | } | |
| 999 | 1003 | ||
| 1004 | + | func TestTestShellToasterCleanupCancelsAutoDismiss(t *testing.T) { | |
| 1005 | + | toaster, box, label := newTestShellToaster(t) | |
| 1006 | + | box.EXPECT().SetVisible(true).Once() | |
| 1007 | + | label.EXPECT().SetText("will not outlive this test").Once() | |
| 1008 | + | toaster.Show(context.Background(), "will not outlive this test", component.ToastInfo) | |
| 1009 | + | toaster.CancelAutoDismiss() | |
| 1010 | + | ||
| 1011 | + | mainContext := glib.MainContextDefault() | |
| 1012 | + | deadline := time.Now().Add(component.ToastBriefDurationMs + 100*time.Millisecond) | |
| 1013 | + | for time.Now().Before(deadline) { | |
| 1014 | + | for mainContext.Pending() { | |
| 1015 | + | mainContext.Iteration(false) | |
| 1016 | + | } | |
| 1017 | + | time.Sleep(10 * time.Millisecond) | |
| 1018 | + | } | |
| 1019 | + | assert.True(t, toaster.IsVisible(), "canceling must prevent the deferred hide callback") | |
| 1020 | + | } | |
| 1021 | + | ||
| 1000 | 1022 | func TestApp_CheckConfigMigrationUsesLastFocusedBrowserWindowToaster(t *testing.T) { | |
| 1001 | 1023 | ctx := context.Background() | |
| 1002 | 1024 | toaster, box, label := newTestShellToaster(t) | |
| @@ -35,7 +35,9 @@ import ( | |||||
| 35 | "github.com/bnema/dumber/internal/ui/window" | 35 | "github.com/bnema/dumber/internal/ui/window" | ||
| 36 | "github.com/bnema/puregotk/v4/gdk" | 36 | "github.com/bnema/puregotk/v4/gdk" | ||
| 37 | "github.com/bnema/puregotk/v4/gio" | 37 | "github.com/bnema/puregotk/v4/gio" | ||
| 38 | + | "github.com/bnema/puregotk/v4/glib" | |||
| 38 | "github.com/bnema/puregotk/v4/gtk" | 39 | "github.com/bnema/puregotk/v4/gtk" | ||
| 40 | + | "github.com/stretchr/testify/assert" | |||
| 39 | "github.com/stretchr/testify/mock" | 41 | "github.com/stretchr/testify/mock" | ||
| 40 | "github.com/stretchr/testify/require" | 42 | "github.com/stretchr/testify/require" | ||
| 41 | ) | 43 | ) | ||
| @@ -979,7 +981,9 @@ func newTestShellToaster(t *testing.T) (*component.Toaster, *layoutmocks.MockBox | |||||
| 979 | label.EXPECT().SetCanTarget(false).Once() | 981 | label.EXPECT().SetCanTarget(false).Once() | ||
| 980 | label.EXPECT().SetCanFocus(false).Once() | 982 | label.EXPECT().SetCanFocus(false).Once() | ||
| 981 | 983 | ||||
| 982 | - | return component.NewToaster(factory), box, label | 984 | + | toaster := component.NewToaster(factory) |
| 985 | + | t.Cleanup(toaster.CancelAutoDismiss) | |||
| 986 | + | return toaster, box, label | |||
| 983 | } | 987 | } | ||
| 984 | 988 | ||||
| 985 | func TestApp_ShowFilterStatusUsesLastFocusedBrowserWindowToaster(t *testing.T) { | 989 | func TestApp_ShowFilterStatusUsesLastFocusedBrowserWindowToaster(t *testing.T) { | ||
| @@ -997,6 +1001,24 @@ func TestApp_ShowFilterStatusUsesLastFocusedBrowserWindowToaster(t *testing.T) { | |||||
| 997 | app.showFilterStatus(ctx, port.FilterStatus{State: port.FilterStateLoading, Message: "Ad blocker loading"}) | 1001 | app.showFilterStatus(ctx, port.FilterStatus{State: port.FilterStateLoading, Message: "Ad blocker loading"}) | ||
| 998 | } | 1002 | } | ||
| 999 | 1003 | ||||
| 1004 | + | func TestTestShellToasterCleanupCancelsAutoDismiss(t *testing.T) { | |||
| 1005 | + | toaster, box, label := newTestShellToaster(t) | |||
| 1006 | + | box.EXPECT().SetVisible(true).Once() | |||
| 1007 | + | label.EXPECT().SetText("will not outlive this test").Once() | |||
| 1008 | + | toaster.Show(context.Background(), "will not outlive this test", component.ToastInfo) | |||
| 1009 | + | toaster.CancelAutoDismiss() | |||
| 1010 | + | ||||
| 1011 | + | mainContext := glib.MainContextDefault() | |||
| 1012 | + | deadline := time.Now().Add(component.ToastBriefDurationMs + 100*time.Millisecond) | |||
| 1013 | + | for time.Now().Before(deadline) { | |||
| 1014 | + | for mainContext.Pending() { | |||
| 1015 | + | mainContext.Iteration(false) | |||
| 1016 | + | } | |||
| 1017 | + | time.Sleep(10 * time.Millisecond) | |||
| 1018 | + | } | |||
| 1019 | + | assert.True(t, toaster.IsVisible(), "canceling must prevent the deferred hide callback") | |||
| 1020 | + | } | |||
| 1021 | + | ||||
| 1000 | func TestApp_CheckConfigMigrationUsesLastFocusedBrowserWindowToaster(t *testing.T) { | 1022 | func TestApp_CheckConfigMigrationUsesLastFocusedBrowserWindowToaster(t *testing.T) { | ||
| 1001 | ctx := context.Background() | 1023 | ctx := context.Background() | ||
| 1002 | toaster, box, label := newTestShellToaster(t) | 1024 | toaster, box, label := newTestShellToaster(t) | ||
M internal/ui/component/toaster.go +12 -0
| @@ -276,6 +276,18 @@ func (t *Toaster) Hide() { | |||
| 276 | 276 | t.hide() | |
| 277 | 277 | } | |
| 278 | 278 | ||
| 279 | + | // CancelAutoDismiss removes a pending auto-dismiss source without changing the | |
| 280 | + | // toast's visibility. It is useful when the owner is being torn down. | |
| 281 | + | func (t *Toaster) CancelAutoDismiss() { | |
| 282 | + | t.mu.Lock() | |
| 283 | + | defer t.mu.Unlock() | |
| 284 | + | ||
| 285 | + | if t.dismissTimer != 0 { | |
| 286 | + | glib.SourceRemove(t.dismissTimer) | |
| 287 | + | t.dismissTimer = 0 | |
| 288 | + | } | |
| 289 | + | } | |
| 290 | + | ||
| 279 | 291 | // hide performs the actual hide operation (must be called with lock held). | |
| 280 | 292 | func (t *Toaster) hide() { | |
| 281 | 293 | if !t.visible { | |
| @@ -276,6 +276,18 @@ func (t *Toaster) Hide() { | |||||
| 276 | t.hide() | 276 | t.hide() | ||
| 277 | } | 277 | } | ||
| 278 | 278 | ||||
| 279 | + | // CancelAutoDismiss removes a pending auto-dismiss source without changing the | |||
| 280 | + | // toast's visibility. It is useful when the owner is being torn down. | |||
| 281 | + | func (t *Toaster) CancelAutoDismiss() { | |||
| 282 | + | t.mu.Lock() | |||
| 283 | + | defer t.mu.Unlock() | |||
| 284 | + | ||||
| 285 | + | if t.dismissTimer != 0 { | |||
| 286 | + | glib.SourceRemove(t.dismissTimer) | |||
| 287 | + | t.dismissTimer = 0 | |||
| 288 | + | } | |||
| 289 | + | } | |||
| 290 | + | ||||
| 279 | // hide performs the actual hide operation (must be called with lock held). | 291 | // hide performs the actual hide operation (must be called with lock held). | ||
| 280 | func (t *Toaster) hide() { | 292 | func (t *Toaster) hide() { | ||
| 281 | if !t.visible { | 293 | if !t.visible { | ||