1package component
2
3import (
4 "testing"
5
6 "github.com/bnema/puregotk/v4/gtk"
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/mock"
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/dumber/internal/application/port"
12 "github.com/bnema/dumber/internal/ui/layout"
13 "github.com/bnema/dumber/internal/ui/layout/mocks"
14)
15
16// fakeJSDialogView records calls made by PaneView.
17type fakeJSDialogView struct {
18 respond func(bool, string)
19 focus bool
20 shown int
21 focusRequests int
22 destroyed bool
23 hiddenCleared bool
24 dismissedWith []bool
25}
26
27func (f *fakeJSDialogView) Widget() *gtk.Widget { return nil }
28func (f *fakeJSDialogView) Show(_ port.JSDialogRequest, respond func(bool, string), focus bool, _ float64) {
29 f.shown++
30 f.respond, f.focus = respond, focus
31}
32func (f *fakeJSDialogView) Visible() bool { return f.respond != nil }
33func (f *fakeJSDialogView) Hide() {}
34func (f *fakeJSDialogView) RequestFocus() { f.focusRequests++ }
35func (f *fakeJSDialogView) SetOnHidden(fn func(bool)) {
36 if fn == nil {
37 f.hiddenCleared = true
38 }
39}
40func (f *fakeJSDialogView) Dismiss(ok bool) {
41 f.dismissedWith = append(f.dismissedWith, ok)
42 if f.respond != nil {
43 r := f.respond
44 f.respond = nil
45 r(ok, "")
46 }
47}
48func (f *fakeJSDialogView) Destroy() { f.destroyed = true }
49
50func newJSDialogPaneView(t *testing.T, fake *fakeJSDialogView) *PaneView {
51 t.Helper()
52 overlay := mocks.NewMockOverlayWidget(t)
53 factory := mocks.NewMockWidgetFactory(t)
54 wrapped := mocks.NewMockWidget(t)
55 factory.EXPECT().WrapWidget(mock.Anything).Return(wrapped).Maybe()
56 overlay.EXPECT().AddOverlay(wrapped).Maybe()
57 overlay.EXPECT().SetClipOverlay(wrapped, true).Maybe()
58 overlay.EXPECT().SetMeasureOverlay(wrapped, false).Maybe()
59 overlay.EXPECT().RemoveOverlay(wrapped).Maybe()
60 pv := &PaneView{
61 overlay: overlay,
62 factory: factory,
63 newJSDialog: func(layout.OverlayWidget) jsDialogView {
64 return fake
65 },
66 }
67 return pv
68}
69
70func TestPaneViewCleanupAnswersOpenJSDialog(t *testing.T) {
71 fake := &fakeJSDialogView{}
72 pv := newJSDialogPaneView(t, fake)
73
74 var answers []bool
75 require.True(t, pv.ShowJSDialog(port.JSDialogRequest{}, func(ok bool, _ string) { answers = append(answers, ok) }, 1))
76
77 pv.Cleanup()
78
79 assert.Equal(t, []bool{false}, answers, "page must be answered, not left blocked")
80 assert.True(t, fake.hiddenCleared, "hidden callback detached before dismissing")
81 assert.True(t, fake.destroyed)
82 assert.Nil(t, pv.jsDialog)
83
84 // Cleanup again is a no-op.
85 pv.Cleanup()
86 assert.Equal(t, []bool{false}, answers)
87}
88
89func TestPaneViewShowJSDialogFocusFollowsActive(t *testing.T) {
90 fake := &fakeJSDialogView{}
91 pv := newJSDialogPaneView(t, fake)
92
93 require.True(t, pv.ShowJSDialog(port.JSDialogRequest{}, func(bool, string) {}, 1))
94 assert.False(t, fake.focus, "inactive pane's dialog must not steal focus")
95
96 pv.isActive = true
97 require.True(t, pv.ShowJSDialog(port.JSDialogRequest{}, func(bool, string) {}, 1))
98 assert.True(t, fake.focus)
99}
100
101func TestPaneViewSetActiveFocusesPendingJSDialog(t *testing.T) {
102 fake := &fakeJSDialogView{}
103 pv := newJSDialogPaneView(t, fake)
104 border := mocks.NewMockBoxWidget(t)
105 border.EXPECT().AddCssClass(activePaneClass).Once()
106 pv.borderBox = border
107 require.True(t, pv.ShowJSDialog(port.JSDialogRequest{}, func(bool, string) {}, 1))
108
109 pv.SetActive(true)
110
111 assert.Equal(t, 1, fake.focusRequests)
112}
113
114func TestPaneViewGrabFocusDefersToVisibleJSDialog(t *testing.T) {
115 fake := &fakeJSDialogView{}
116 pv := newJSDialogPaneView(t, fake)
117 webview := mocks.NewMockWidget(t) // GrabFocus on it would fail the test (no expectation)
118 pv.webViewWidget = webview
119 border := mocks.NewMockBoxWidget(t)
120 border.EXPECT().AddCssClass(activePaneClass).Once()
121 pv.borderBox = border
122 require.True(t, pv.ShowJSDialog(port.JSDialogRequest{}, func(bool, string) {}, 1))
123
124 pv.SetActive(true)
125 require.Equal(t, 1, fake.focusRequests)
126 assert.True(t, pv.GrabFocus())
127
128 assert.Equal(t, 2, fake.focusRequests, "dialog focus requested again, WebView not focused")
129}
130
131func TestPaneViewGrabFocusFocusesWebViewWithoutDialog(t *testing.T) {
132 fake := &fakeJSDialogView{}
133 pv := newJSDialogPaneView(t, fake)
134 webview := mocks.NewMockWidget(t)
135 webview.EXPECT().GrabFocus().Return(true).Once()
136 pv.webViewWidget = webview
137
138 assert.True(t, pv.GrabFocus())
139}
140
141func TestPaneViewShowJSDialogAfterCleanupFails(t *testing.T) {
142 fake := &fakeJSDialogView{}
143 pv := newJSDialogPaneView(t, fake)
144 pv.Cleanup()
145
146 assert.False(t, pv.ShowJSDialog(port.JSDialogRequest{}, func(bool, string) {}, 1))
147 assert.Zero(t, fake.shown)
148}