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