1package component_test
2
3import (
4 "context"
5 "testing"
6
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/domain/entity"
12 "github.com/bnema/dumber/internal/ui/component"
13 "github.com/bnema/dumber/internal/ui/layout"
14 "github.com/bnema/dumber/internal/ui/layout/mocks"
15)
16
17func setupWorkspaceViewBase(t *testing.T, mockFactory *mocks.MockWidgetFactory) (context.Context, *mocks.MockBoxWidget) {
18 ctx := context.Background()
19 mockContainer := mocks.NewMockBoxWidget(t)
20 mockOverlay := mocks.NewMockOverlayWidget(t)
21
22 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockContainer).Once()
23 mockContainer.EXPECT().SetHexpand(true).Once()
24 mockContainer.EXPECT().SetVexpand(true).Once()
25 mockContainer.EXPECT().SetVisible(true).Once()
26
27 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
28 mockOverlay.EXPECT().SetHexpand(true).Once()
29 mockOverlay.EXPECT().SetVexpand(true).Once()
30 mockOverlay.EXPECT().SetChild(mockContainer).Once()
31 mockOverlay.EXPECT().SetVisible(true).Once()
32
33 return ctx, mockContainer
34}
35
36func setupWorkspacePaneViewMocks(t *testing.T, mockFactory *mocks.MockWidgetFactory) (*mocks.MockOverlayWidget, *mocks.MockBoxWidget) {
37 mockOverlay := mocks.NewMockOverlayWidget(t)
38 mockBorderBox := mocks.NewMockBoxWidget(t)
39
40 mockFactory.EXPECT().NewOverlay().Return(mockOverlay).Once()
41 mockOverlay.EXPECT().SetHexpand(true).Once()
42 mockOverlay.EXPECT().SetVexpand(true).Once()
43 mockOverlay.EXPECT().SetVisible(true).Once()
44 mockOverlay.EXPECT().AddCssClass("pane-overlay").Once()
45
46 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockBorderBox).Once()
47 mockBorderBox.EXPECT().SetCanFocus(false).Once()
48 mockBorderBox.EXPECT().SetCanTarget(false).Once()
49 mockBorderBox.EXPECT().AddCssClass("pane-border").Once()
50 mockBorderBox.EXPECT().SetHexpand(true).Once()
51 mockBorderBox.EXPECT().SetVexpand(true).Once()
52 mockOverlay.EXPECT().AddOverlay(mockBorderBox).Once()
53 mockOverlay.EXPECT().SetClipOverlay(mockBorderBox, false).Once()
54 mockOverlay.EXPECT().SetMeasureOverlay(mockBorderBox, false).Once()
55
56 // GtkWidget is called when attaching hover handler - return nil for tests
57 mockOverlay.EXPECT().GtkWidget().Return(nil).Once()
58
59 return mockOverlay, mockBorderBox
60}
61
62func setupStackedLeafMocks(
63 t *testing.T,
64 mockFactory *mocks.MockWidgetFactory,
65 container *mocks.MockOverlayWidget,
66) *mocks.MockBoxWidget {
67 mockStackBox := mocks.NewMockBoxWidget(t)
68 mockTitleBar := mocks.NewMockBoxWidget(t)
69 mockFavicon := mocks.NewMockImageWidget(t)
70 mockLabel := mocks.NewMockLabelWidget(t)
71 mockButton := mocks.NewMockButtonWidget(t)
72
73 mockFactory.EXPECT().NewBox(layout.OrientationVertical, 0).Return(mockStackBox).Once()
74 mockStackBox.EXPECT().SetHexpand(true).Once()
75 mockStackBox.EXPECT().SetVexpand(true).Once()
76
77 mockFactory.EXPECT().NewBox(layout.OrientationHorizontal, 4).Return(mockTitleBar).Once()
78 mockTitleBar.EXPECT().AddCssClass("stacked-pane-titlebar").Once()
79 mockTitleBar.EXPECT().SetVexpand(false).Once()
80
81 mockFactory.EXPECT().NewImage().Return(mockFavicon).Once()
82 mockFavicon.EXPECT().SetFromIconName(mock.Anything).Once()
83 mockFavicon.EXPECT().SetPixelSize(16).Once()
84 mockTitleBar.EXPECT().Append(mockFavicon).Once()
85
86 mockFactory.EXPECT().NewLabel(mock.Anything).Return(mockLabel).Once()
87 mockLabel.EXPECT().SetEllipsize(layout.EllipsizeEnd).Once()
88 mockLabel.EXPECT().SetMaxWidthChars(30).Once()
89 mockLabel.EXPECT().SetHexpand(true).Once()
90 mockLabel.EXPECT().SetXalign(float32(0.0)).Once()
91 mockTitleBar.EXPECT().Append(mockLabel).Once()
92
93 mockFactory.EXPECT().NewButton().Return(mockButton).Once()
94 mockButton.EXPECT().SetChild(mockTitleBar).Once()
95 mockButton.EXPECT().AddCssClass("stacked-pane-title-button").Once()
96 mockButton.EXPECT().SetFocusOnClick(false).Once()
97 mockButton.EXPECT().SetVexpand(false).Once()
98 mockButton.EXPECT().SetHexpand(true).Once()
99 mockButton.EXPECT().ConnectClicked(mock.Anything).Return(uint32(1)).Once()
100
101 mockStackBox.EXPECT().Append(mockButton).Once()
102 mockStackBox.EXPECT().Append(container).Once()
103
104 mockTitleBar.EXPECT().GetParent().Return(nil).Maybe()
105 container.EXPECT().SetVisible(true).Once()
106 mockTitleBar.EXPECT().AddCssClass("active").Once()
107
108 return mockStackBox
109}
110
111func TestNewWorkspaceView_CreatesEmptyContainer(t *testing.T) {
112 // Arrange
113 mockFactory := mocks.NewMockWidgetFactory(t)
114 ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
115
116 // Act
117 wv := component.NewWorkspaceView(ctx, mockFactory)
118
119 // Assert
120 require.NotNil(t, wv)
121 assert.Equal(t, 0, wv.PaneCount())
122 assert.Equal(t, mockBox, wv.Container())
123}
124
125func TestSetWorkspace_NilWorkspace_ReturnsError(t *testing.T) {
126 // Arrange
127 mockFactory := mocks.NewMockWidgetFactory(t)
128 ctx, _ := setupWorkspaceViewBase(t, mockFactory)
129
130 wv := component.NewWorkspaceView(ctx, mockFactory)
131
132 // Act
133 err := wv.SetWorkspace(ctx, nil)
134
135 // Assert
136 assert.ErrorIs(t, err, component.ErrNilWorkspace)
137}
138
139func TestSetWorkspace_SinglePane_CreatesPaneView(t *testing.T) {
140 // Arrange
141 mockFactory := mocks.NewMockWidgetFactory(t)
142 ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
143
144 wv := component.NewWorkspaceView(ctx, mockFactory)
145
146 mockOverlay, mockBorderBox := setupWorkspacePaneViewMocks(t, mockFactory)
147 mockStackBox := setupStackedLeafMocks(t, mockFactory, mockOverlay)
148
149 mockStackBox.EXPECT().SetVisible(true).Once()
150 mockBox.EXPECT().Append(mockStackBox).Once()
151 mockBox.EXPECT().AddCssClass("single-pane").Once()
152 mockBorderBox.EXPECT().AddCssClass("pane-active").Once()
153
154 pane := entity.NewPane(entity.PaneID("pane-1"))
155 node := &entity.PaneNode{ID: "node-1", Pane: pane}
156
157 ws := &entity.Workspace{
158 ID: "ws-1",
159 Root: node,
160 ActivePaneID: pane.ID,
161 }
162
163 // Act
164 err := wv.SetWorkspace(ctx, ws)
165
166 // Assert
167 require.NoError(t, err)
168 assert.Equal(t, 1, wv.PaneCount())
169 assert.Equal(t, pane.ID, wv.GetActivePaneID())
170 assert.NotNil(t, wv.GetPaneView(pane.ID))
171}
172
173func TestSetActivePaneID_UpdatesStyling(t *testing.T) {
174 // Arrange
175 mockFactory := mocks.NewMockWidgetFactory(t)
176 ctx, mockBox := setupWorkspaceViewBase(t, mockFactory)
177
178 wv := component.NewWorkspaceView(ctx, mockFactory)
179
180 // Setup two panes
181 pane1 := entity.NewPane(entity.PaneID("pane-1"))
182 pane2 := entity.NewPane(entity.PaneID("pane-2"))
183 node1 := &entity.PaneNode{ID: "node-1", Pane: pane1}
184 node2 := &entity.PaneNode{ID: "node-2", Pane: pane2}
185
186 // Create mock widgets for split
187 mockPaned := mocks.NewMockPanedWidget(t)
188 mockOverlay1, mockBorderBox1 := setupWorkspacePaneViewMocks(t, mockFactory)
189 mockStackBox1 := setupStackedLeafMocks(t, mockFactory, mockOverlay1)
190 mockOverlay2, mockBorderBox2 := setupWorkspacePaneViewMocks(t, mockFactory)
191 mockStackBox2 := setupStackedLeafMocks(t, mockFactory, mockOverlay2)
192
193 // Split view creation
194 mockFactory.EXPECT().NewPaned(layout.OrientationHorizontal).Return(mockPaned).Once()
195 mockPaned.EXPECT().SetResizeStartChild(true).Once()
196 mockPaned.EXPECT().SetResizeEndChild(true).Once()
197 mockPaned.EXPECT().SetVisible(true).Twice()
198 mockStackBox1.EXPECT().SetVisible(true).Once()
199 mockStackBox2.EXPECT().SetVisible(true).Once()
200 mockPaned.EXPECT().SetStartChild(mockStackBox1).Once()
201 mockPaned.EXPECT().SetEndChild(mockStackBox2).Once()
202 mockPaned.EXPECT().GetAllocatedWidth().Return(0).Once()
203 mockPaned.EXPECT().ConnectMap(mock.Anything).Return(uint32(0)).Once()
204 mockPaned.EXPECT().AddTickCallback(mock.Anything).Return(uint(0)).Once()
205
206 mockBox.EXPECT().Append(mockPaned).Once()
207
208 // Initial active pane is pane-1
209 mockBox.EXPECT().RemoveCssClass("single-pane").Once()
210 mockBorderBox1.EXPECT().AddCssClass("pane-active").Once()
211
212 splitNode := &entity.PaneNode{
213 ID: "split",
214 SplitDir: entity.SplitHorizontal,
215 SplitRatio: 0.5,
216 Children: []*entity.PaneNode{node1, node2},
217 }
218
219 ws := &entity.Workspace{
220 ID: "ws-1",
221 Root: splitNode,
222 ActivePaneID: pane1.ID,
223 }
224
225 err := wv.SetWorkspace(ctx, ws)
226 require.NoError(t, err)
227
228 // Now change active pane
229 mockBorderBox1.EXPECT().RemoveCssClass("pane-active").Once()
230 mockBorderBox2.EXPECT().AddCssClass("pane-active").Once()
231
232 // Act
233 err = wv.SetActivePaneID(pane2.ID)
234
235 // Assert
236 require.NoError(t, err)
237 assert.Equal(t, pane2.ID, wv.GetActivePaneID())
238}
239
240func TestSetActivePaneID_InvalidID_ReturnsError(t *testing.T) {
241 // Arrange
242 mockFactory := mocks.NewMockWidgetFactory(t)
243 ctx, _ := setupWorkspaceViewBase(t, mockFactory)
244
245 wv := component.NewWorkspaceView(ctx, mockFactory)
246
247 // Act
248 err := wv.SetActivePaneID(entity.PaneID("non-existent"))
249
250 // Assert
251 assert.ErrorIs(t, err, component.ErrPaneNotFound)
252}