omnibox_initial_behavior_test.go RawHistoryFolder
1package component
2
3import (
4 "context"
5 "errors"
6 "testing"
7 "time"
8
9 appusecase "github.com/bnema/dumber/internal/application/usecase"
10 "github.com/bnema/dumber/internal/domain/entity"
11 repository "github.com/bnema/dumber/internal/domain/repository"
12 repomocks "github.com/bnema/dumber/internal/domain/repository/mocks"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/mock"
15)
16
17func TestOmniboxInitialBehaviorBadgeState(t *testing.T) {
18 tests := []struct {
19 name string
20 behavior entity.OmniboxInitialBehavior
21 wantVisible bool
22 wantLabel string
23 wantTooltip string
24 wantNext entity.OmniboxInitialBehavior
25 }{
26 {
27 name: "recent",
28 behavior: entity.OmniboxInitialBehaviorRecent,
29 wantVisible: true,
30 wantLabel: "Recent",
31 wantTooltip: "Default history order · Ctrl+R to switch",
32 wantNext: entity.OmniboxInitialBehaviorMostVisited,
33 },
34 {
35 name: "most visited",
36 behavior: entity.OmniboxInitialBehaviorMostVisited,
37 wantVisible: true,
38 wantLabel: "Most used",
39 wantTooltip: "Default history order · Ctrl+R to switch",
40 wantNext: entity.OmniboxInitialBehaviorRecent,
41 },
42 {
43 name: "none",
44 behavior: entity.OmniboxInitialBehaviorNone,
45 },
46 {
47 name: "invalid",
48 behavior: entity.OmniboxInitialBehavior("unsupported"),
49 },
50 }
51
52 for _, tt := range tests {
53 t.Run(tt.name, func(t *testing.T) {
54 got := initialBehaviorBadgeState(tt.behavior)
55 assert.Equal(t, tt.wantVisible, got.visible)
56 assert.Equal(t, tt.wantLabel, got.label)
57 assert.Equal(t, tt.wantTooltip, got.tooltip)
58 assert.Equal(t, tt.wantNext, got.nextBehavior)
59 })
60 }
61}
62
63func TestShouldRefreshInitialHistoryAfterToggle(t *testing.T) {
64 tests := []struct {
65 name string
66 viewMode ViewMode
67 query string
68 behavior entity.OmniboxInitialBehavior
69 wantRefresh bool
70 }{
71 {
72 name: "empty history mode",
73 viewMode: ViewModeHistory,
74 query: "",
75 behavior: entity.OmniboxInitialBehaviorRecent,
76 wantRefresh: true,
77 },
78 {
79 name: "typed query",
80 viewMode: ViewModeHistory,
81 query: "go",
82 behavior: entity.OmniboxInitialBehaviorRecent,
83 wantRefresh: false,
84 },
85 {
86 name: "favorites mode",
87 viewMode: ViewModeFavorites,
88 query: "",
89 behavior: entity.OmniboxInitialBehaviorRecent,
90 wantRefresh: false,
91 },
92 {
93 name: "unsupported behavior",
94 viewMode: ViewModeHistory,
95 query: "",
96 behavior: entity.OmniboxInitialBehaviorNone,
97 wantRefresh: false,
98 },
99 }
100
101 for _, tt := range tests {
102 t.Run(tt.name, func(t *testing.T) {
103 assert.Equal(t, tt.wantRefresh, shouldRefreshInitialHistoryAfterToggle(tt.viewMode, tt.query, tt.behavior))
104 })
105 }
106}
107
108func TestOmniboxToggleInitialBehaviorPreference_SaveFailureKeepsBehaviorAndShowsToast(t *testing.T) {
109 o := &Omnibox{initialBehavior: entity.OmniboxInitialBehaviorRecent, ctx: context.Background()}
110 o.saveInitialBehaviorFn = func(context.Context, entity.OmniboxInitialBehavior) error {
111 return errors.New("boom")
112 }
113
114 var gotMessage string
115 var gotLevel ToastLevel
116 o.onToast = func(_ context.Context, message string, level ToastLevel) {
117 gotMessage = message
118 gotLevel = level
119 }
120
121 refreshCalls := 0
122 origLoadInitialHistoryFn := loadInitialHistoryFn
123 loadInitialHistoryFn = func(*Omnibox, uint64) {
124 refreshCalls++
125 }
126 t.Cleanup(func() {
127 loadInitialHistoryFn = origLoadInitialHistoryFn
128 })
129
130 assert.False(t, o.toggleInitialBehaviorPreference())
131 assert.Equal(t, entity.OmniboxInitialBehaviorRecent, o.initialBehavior)
132 assert.Equal(t, "Failed to save default history order", gotMessage)
133 assert.Equal(t, ToastError, gotLevel)
134 assert.Equal(t, 0, refreshCalls)
135}
136
137func TestOmniboxToggleInitialBehaviorPreference_RefreshesEmptyHistory(t *testing.T) {
138 o := &Omnibox{initialBehavior: entity.OmniboxInitialBehaviorRecent, viewMode: ViewModeHistory, ctx: context.Background()}
139 o.saveInitialBehaviorFn = func(context.Context, entity.OmniboxInitialBehavior) error {
140 return nil
141 }
142
143 refreshCalls := 0
144 origLoadInitialHistoryFn := loadInitialHistoryFn
145 loadInitialHistoryFn = func(*Omnibox, uint64) {
146 refreshCalls++
147 }
148 t.Cleanup(func() {
149 loadInitialHistoryFn = origLoadInitialHistoryFn
150 })
151
152 assert.True(t, o.toggleInitialBehaviorPreference())
153 assert.Equal(t, entity.OmniboxInitialBehaviorMostVisited, o.initialBehavior)
154 assert.Equal(t, 1, refreshCalls)
155}
156
157// omniboxSuggestionUpdatesInline makes suggestion refreshes run synchronously
158// on the calling goroutine instead of through the GTK main loop.
159func omniboxSuggestionUpdatesInline(o *Omnibox) {
160 o.scheduleSuggestionUpdate = func(fn func()) { fn() }
161}
162
163// scopeCutoffWithin matches a history scope whose cutoff is approximately the
164// given age, tolerating the small delay between test setup and query execution.
165func scopeCutoffWithin(want, tolerance time.Duration) any {
166 return mock.MatchedBy(func(scope repository.HistoryScope) bool {
167 if scope.Cutoff.IsZero() {
168 return false
169 }
170 diff := time.Since(scope.Cutoff) - want
171 if diff < 0 {
172 diff = -diff
173 }
174 return diff <= tolerance
175 })
176}
177
178// scopeUnbounded matches a history scope with no age restriction.
179func scopeUnbounded() any {
180 return mock.MatchedBy(func(scope repository.HistoryScope) bool {
181 return scope.Cutoff.IsZero()
182 })
183}
184
185func TestOmniboxLoadInitialHistory_UsesCapturedInitialBehavior(t *testing.T) {
186 repo := repomocks.NewMockHistoryRepository(t)
187 done := make(chan struct{})
188 repo.EXPECT().
189 GetRecent(mock.Anything, mock.Anything, 0, mock.Anything).
190 Run(func(context.Context, int, int, repository.HistoryScope) { close(done) }).
191 Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil)
192
193 o := &Omnibox{
194 historyUC: appusecase.NewSearchHistoryUseCase(repo),
195 initialBehavior: entity.OmniboxInitialBehaviorRecent,
196 maxHistoryDays: 30,
197 ctx: context.Background(),
198 }
199 omniboxSuggestionUpdatesInline(o)
200
201 o.loadInitialHistory(1)
202 o.initialBehavior = entity.OmniboxInitialBehaviorMostVisited
203
204 select {
205 case <-done:
206 case <-time.After(2 * time.Second):
207 t.Fatal("timed out waiting for recent-history load")
208 }
209}
210
211func TestOmniboxLoadInitialHistory_RecentUsesConfiguredWindow(t *testing.T) {
212 repo := repomocks.NewMockHistoryRepository(t)
213 done := make(chan struct{})
214 repo.EXPECT().
215 GetRecent(mock.Anything, mock.Anything, 0, scopeCutoffWithin(7*24*time.Hour, time.Minute)).
216 Run(func(context.Context, int, int, repository.HistoryScope) { close(done) }).
217 Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil)
218
219 o := &Omnibox{
220 historyUC: appusecase.NewSearchHistoryUseCase(repo),
221 initialBehavior: entity.OmniboxInitialBehaviorRecent,
222 maxHistoryDays: 7,
223 ctx: context.Background(),
224 }
225 omniboxSuggestionUpdatesInline(o)
226
227 o.loadInitialHistory(1)
228
229 select {
230 case <-done:
231 case <-time.After(2 * time.Second):
232 t.Fatal("timed out waiting for recent-history load")
233 }
234}
235
236func TestOmniboxLoadInitialHistory_MostVisitedUsesConfiguredWindow(t *testing.T) {
237 repo := repomocks.NewMockHistoryRepository(t)
238 done := make(chan struct{})
239 repo.EXPECT().
240 GetMostVisitedWithin(mock.Anything, mock.Anything, scopeCutoffWithin(7*24*time.Hour, time.Minute)).
241 Run(func(context.Context, int, repository.HistoryScope) { close(done) }).
242 Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil)
243
244 o := &Omnibox{
245 historyUC: appusecase.NewSearchHistoryUseCase(repo),
246 initialBehavior: entity.OmniboxInitialBehaviorMostVisited,
247 maxHistoryDays: 7,
248 ctx: context.Background(),
249 }
250 omniboxSuggestionUpdatesInline(o)
251
252 o.loadInitialHistory(1)
253
254 select {
255 case <-done:
256 case <-time.After(2 * time.Second):
257 t.Fatal("timed out waiting for configured most-visited load")
258 }
259}
260
261func TestOmniboxLoadInitialHistory_MostVisitedZeroWindowUsesAllHistory(t *testing.T) {
262 repo := repomocks.NewMockHistoryRepository(t)
263 done := make(chan struct{})
264 repo.EXPECT().
265 GetMostVisitedWithin(mock.Anything, mock.Anything, scopeUnbounded()).
266 Run(func(context.Context, int, repository.HistoryScope) { close(done) }).
267 Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil)
268
269 o := &Omnibox{
270 historyUC: appusecase.NewSearchHistoryUseCase(repo),
271 initialBehavior: entity.OmniboxInitialBehaviorMostVisited,
272 maxHistoryDays: 0,
273 ctx: context.Background(),
274 }
275 omniboxSuggestionUpdatesInline(o)
276
277 o.loadInitialHistory(1)
278
279 select {
280 case <-done:
281 case <-time.After(2 * time.Second):
282 t.Fatal("timed out waiting for all-history most-visited load")
283 }
284}
285
286func TestOmniboxLoadInitialHistory_MostVisitedBoundsSQL(t *testing.T) {
287 repo := repomocks.NewMockHistoryRepository(t)
288 done := make(chan struct{})
289 repo.EXPECT().
290 GetMostVisitedWithin(mock.Anything, OmniboxListDefaults.MaxResults, mock.Anything).
291 Run(func(context.Context, int, repository.HistoryScope) { close(done) }).
292 Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil)
293
294 o := &Omnibox{
295 historyUC: appusecase.NewSearchHistoryUseCase(repo),
296 initialBehavior: entity.OmniboxInitialBehaviorMostVisited,
297 maxHistoryDays: 30,
298 ctx: context.Background(),
299 }
300 omniboxSuggestionUpdatesInline(o)
301
302 o.loadInitialHistory(1)
303
304 select {
305 case <-done:
306 case <-time.After(2 * time.Second):
307 t.Fatal("timed out waiting for bounded most-visited load")
308 }
309}
310
311func TestOmniboxSearchHistory_PassesConfiguredAgeScope(t *testing.T) {
312 repo := repomocks.NewMockHistoryRepository(t)
313 done := make(chan struct{})
314 repo.EXPECT().
315 Search(mock.Anything, "example", 5, scopeCutoffWithin(30*24*time.Hour, time.Minute)).
316 Run(func(context.Context, string, int, repository.HistoryScope) { close(done) }).
317 Return([]entity.HistoryMatch{}, nil)
318
319 o := &Omnibox{
320 historyUC: appusecase.NewSearchHistoryUseCase(repo),
321 maxHistoryDays: 30,
322 ctx: context.Background(),
323 }
324 omniboxSuggestionUpdatesInline(o)
325
326 o.searchHistory("example", 5, 1)
327
328 select {
329 case <-done:
330 case <-time.After(2 * time.Second):
331 t.Fatal("timed out waiting for typed history search")
332 }
333}