1package ui
2
3import (
4 "context"
5 "errors"
6 "testing"
7 "time"
8
9 "github.com/bnema/dumber/internal/application/port"
10 portmocks "github.com/bnema/dumber/internal/application/port/mocks"
11 "github.com/bnema/dumber/internal/shared/syncdispatch"
12 "github.com/bnema/dumber/internal/ui/coordinator/content"
13 layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
14 "github.com/bnema/dumber/internal/ui/window"
15 "github.com/bnema/puregotk/v4/gtk"
16 "github.com/stretchr/testify/assert"
17 "github.com/stretchr/testify/require"
18)
19
20type popupDestroySpy struct {
21 destroyed bool
22}
23
24func (p *popupDestroySpy) Destroy() {
25 p.destroyed = true
26}
27
28func TestPrepareNativePopupContentWidget_ExpandsWrappedWidget(t *testing.T) {
29 widget := layoutmocks.NewMockWidget(t)
30 gtkWidget := >k.Widget{}
31 widget.EXPECT().GtkWidget().Return(gtkWidget).Once()
32 widget.EXPECT().SetHexpand(true).Once()
33 widget.EXPECT().SetVexpand(true).Once()
34
35 got, err := prepareNativePopupContentWidget(widget)
36 require.NoError(t, err)
37 assert.Same(t, gtkWidget, got)
38}
39
40func TestPrepareNativePopupContentWidget_ErrorsWhenWrappedGTKWidgetMissing(t *testing.T) {
41 widget := layoutmocks.NewMockWidget(t)
42 widget.EXPECT().GtkWidget().Return(nil).Once()
43
44 got, err := prepareNativePopupContentWidget(widget)
45 require.Error(t, err)
46 assert.Nil(t, got)
47}
48
49func TestOpenNativePopupWindow_DestroysWebViewWhenPopupShellCreationFails(t *testing.T) {
50 oldNewPopupWindow := newPopupWindow
51 t.Cleanup(func() {
52 newPopupWindow = oldNewPopupWindow
53 })
54
55 expectedErr := errors.New("boom")
56 newPopupWindow = func(context.Context, *gtk.Application) (*window.PopupWindow, error) {
57 return nil, expectedErr
58 }
59
60 wv := portmocks.NewMockWebView(t)
61 wv.EXPECT().IsDestroyed().Return(false).Once()
62 wv.EXPECT().Destroy().Once()
63
64 app := &App{gtkApp: >k.Application{}}
65 err := app.openNativePopupWindow(context.Background(), content.NativePopupInput{PopupWebView: wv})
66 require.ErrorIs(t, err, expectedErr)
67}
68
69func TestDestroyFailedNativePopupSetup_DestroysWebViewAndShell(t *testing.T) {
70 wv := portmocks.NewMockWebView(t)
71 wv.EXPECT().IsDestroyed().Return(false).Once()
72 wv.EXPECT().Destroy().Once()
73
74 shell := &popupDestroySpy{}
75 destroyFailedNativePopupSetup(shell, wv)
76
77 assert.True(t, shell.destroyed)
78}
79
80func TestDispatchNativePopupLifecycleSkipsWorkWhenDispatchTimesOutBeforeStart(t *testing.T) {
81 var ran bool
82 app := &App{
83 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
84 return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchTimedOut, Elapsed: 5 * time.Millisecond}
85 },
86 }
87
88 app.dispatchNativePopupLifecycle("ui.native_popup.close", port.WebViewID(99), func() { ran = true })
89
90 assert.False(t, ran)
91}
92
93func TestDispatchNativePopupLifecycleTreatsCompletedAfterTimeoutAsCompleted(t *testing.T) {
94 var ran bool
95 app := &App{
96 dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
97 fn()
98 return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompletedAfterTimeout, Elapsed: 10 * time.Millisecond}
99 },
100 }
101
102 app.dispatchNativePopupLifecycle("ui.native_popup.close", port.WebViewID(99), func() { ran = true })
103
104 assert.True(t, ran)
105}
106
107func TestReleaseNativePopupWindow_DestroysWebViewAndRemovesState(t *testing.T) {
108 wv := portmocks.NewMockWebView(t)
109 wv.EXPECT().IsDestroyed().Return(false).Once()
110 wv.EXPECT().Destroy().Once()
111
112 app := &App{nativePopupWindows: map[port.WebViewID]*nativePopupWindow{
113 port.WebViewID(1): {
114 popupID: port.WebViewID(1),
115 webView: wv,
116 },
117 }}
118
119 app.releaseNativePopupWindow(port.WebViewID(1), false, false)
120 _, ok := app.nativePopupWindows[port.WebViewID(1)]
121 assert.False(t, ok)
122}