1package main
2
3import (
4 "context"
5 "strings"
6 "sync/atomic"
7 "testing"
8 "time"
9
10 "github.com/bnema/dumber/internal/application/dto"
11 "github.com/bnema/dumber/internal/application/port"
12 "github.com/bnema/dumber/internal/domain/entity"
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15)
16
17func TestNewBridgeApp_WiresHistoryService(t *testing.T) {
18 t.Parallel()
19
20 bridge := &fakeBridgeService{historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}}}
21 app := newBridgeApp(&fakeDOM{}, "dumb://history", bridge)
22
23 require.NoError(t, app.LoadInitial(context.Background()))
24 assert.True(t, bridge.calledHistory.Load())
25 assert.False(t, bridge.calledFavorites.Load())
26 assert.True(t, bridge.calledConfig.Load())
27 assert.False(t, bridge.calledKeybindings.Load())
28}
29
30func TestNewBridgeApp_UsesCurrentConfigForNonConfigRoutes(t *testing.T) {
31 t.Parallel()
32
33 bridge := &fakeBridgeService{
34 historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}},
35 currentConfig: dto.SystemviewConfigPayload{
36 Appearance: dto.WebUIAppearanceConfig{
37 ColorScheme: "prefer-dark",
38 DarkPalette: dto.ColorPalette{
39 Background: "#111111",
40 Surface: "#1a1a1a",
41 SurfaceVariant: "#2a2a2a",
42 Text: "#f5f5f5",
43 Muted: "#a0a0a0",
44 Accent: "#66aaff",
45 Border: "#333333",
46 },
47 },
48 },
49 }
50 dom := &fakeDOM{mounts: make(chan string, 4)}
51 app := newBridgeApp(dom, "dumb://history", bridge)
52
53 require.NoError(t, app.Run())
54 require.Eventually(t, bridge.calledHistory.Load, time.Second, 10*time.Millisecond)
55 assert.True(t, bridge.calledConfig.Load())
56 assert.False(t, bridge.calledKeybindings.Load())
57 html := receiveMountContaining(t, dom.mounts, `class="sv-app sv-dark"`, `--sv-background: #111111;`)
58 assert.Contains(t, html, `class="sv-app sv-dark"`)
59 assert.Contains(t, html, `--sv-background: #111111;`)
60}
61
62func TestNewBridgeApp_WiresFavoritesService(t *testing.T) {
63 t.Parallel()
64
65 bridge := &fakeBridgeService{
66 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},
67 folders: []*entity.Folder{{Name: "Read Later"}},
68 tags: []*entity.Tag{{Name: "Go"}},
69 }
70 app := newBridgeApp(&fakeDOM{}, "dumb://favorites", bridge)
71
72 require.NoError(t, app.LoadInitial(context.Background()))
73 assert.False(t, bridge.calledHistory.Load())
74 assert.True(t, bridge.calledFavorites.Load())
75 assert.True(t, bridge.calledConfig.Load())
76 assert.False(t, bridge.calledKeybindings.Load())
77}
78
79func TestNewBridgeApp_WiresConfigService(t *testing.T) {
80 t.Parallel()
81
82 bridge := &fakeBridgeService{
83 currentConfig: dto.SystemviewConfigPayload{EngineType: "webkit"},
84 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{DisplayName: "Default"}}},
85 }
86 app := newBridgeApp(&fakeDOM{}, "dumb://config", bridge)
87
88 require.NoError(t, app.LoadInitial(context.Background()))
89 assert.False(t, bridge.calledHistory.Load())
90 assert.False(t, bridge.calledFavorites.Load())
91 assert.True(t, bridge.calledConfig.Load())
92 assert.True(t, bridge.calledKeybindings.Load())
93}
94
95type fakeDOM struct {
96 html string
97 mounts chan string
98}
99
100func (f *fakeDOM) Mount(html string) error {
101 f.html = html
102 if f.mounts != nil {
103 select {
104 case f.mounts <- html:
105 default:
106 }
107 }
108 return nil
109}
110
111func receiveMountContaining(t *testing.T, mounts <-chan string, values ...string) string {
112 t.Helper()
113 deadline := time.After(time.Second)
114 for {
115 select {
116 case html := <-mounts:
117 matched := true
118 for _, value := range values {
119 if !strings.Contains(html, value) {
120 matched = false
121 break
122 }
123 }
124 if matched {
125 return html
126 }
127 case <-deadline:
128 t.Fatalf("timed out waiting for DOM mount containing %q", values)
129 return ""
130 }
131 }
132}
133
134// Handwritten fake intentionally tracks state across history, favorites, config,
135// and keybindings boundaries for the composite bootstrap assertions.
136type fakeBridgeService struct {
137 calledHistory atomic.Bool
138 calledFavorites atomic.Bool
139 calledConfig atomic.Bool
140 calledKeybindings atomic.Bool
141
142 historyEntries []*entity.HistoryEntry
143 favorites []*entity.Favorite
144 folders []*entity.Folder
145 tags []*entity.Tag
146 currentConfig dto.SystemviewConfigPayload
147 keybindings port.KeybindingsConfig
148}
149
150func (f *fakeBridgeService) Timeline(context.Context, int, int) ([]*entity.HistoryEntry, error) {
151 f.calledHistory.Store(true)
152 return f.historyEntries, nil
153}
154
155func (f *fakeBridgeService) TimelineByDomain(context.Context, string, int, int) ([]*entity.HistoryEntry, error) {
156 f.calledHistory.Store(true)
157 return f.historyEntries, nil
158}
159
160func (f *fakeBridgeService) TimelineWindow(_ context.Context, before time.Time, _ string) (*entity.HistoryWindow, error) {
161 f.calledHistory.Store(true)
162 return &entity.HistoryWindow{Entries: f.historyEntries, Before: before, After: before.Add(-24 * time.Hour)}, nil
163}
164
165func (*fakeBridgeService) Search(context.Context, string, int) ([]*entity.HistoryEntry, error) {
166 return nil, nil
167}
168
169func (*fakeBridgeService) DeleteEntry(context.Context, int64) error { return nil }
170
171func (*fakeBridgeService) DeleteRange(context.Context, string) error { return nil }
172
173func (*fakeBridgeService) Stats(context.Context) (*entity.HistoryStats, error) {
174 return nil, nil
175}
176
177func (*fakeBridgeService) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
178 return nil, nil
179}
180
181func (*fakeBridgeService) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
182 return nil, nil
183}
184
185func (*fakeBridgeService) DeleteDomain(context.Context, string) error { return nil }
186
187func (f *fakeBridgeService) List(context.Context) ([]*entity.Favorite, error) {
188 f.calledFavorites.Store(true)
189 return f.favorites, nil
190}
191
192func (*fakeBridgeService) CreateFavorite(context.Context, dto.FavoriteCreateInput) (*entity.Favorite, error) {
193 return nil, nil
194}
195
196func (*fakeBridgeService) UpdateFavorite(context.Context, dto.FavoriteUpdateInput) (*entity.Favorite, error) {
197 return nil, nil
198}
199
200func (*fakeBridgeService) DeleteFavorite(context.Context, int64) error { return nil }
201
202func (f *fakeBridgeService) ListFolders(context.Context) ([]*entity.Folder, error) {
203 return f.folders, nil
204}
205
206func (f *fakeBridgeService) ListTags(context.Context) ([]*entity.Tag, error) {
207 return f.tags, nil
208}
209
210func (*fakeBridgeService) SetShortcut(context.Context, int64, *int) error { return nil }
211
212func (*fakeBridgeService) SetFolder(context.Context, int64, *int64) error { return nil }
213
214func (*fakeBridgeService) CreateFolder(context.Context, string, string, *int64) (*entity.Folder, error) {
215 return nil, nil
216}
217
218func (*fakeBridgeService) UpdateFolder(context.Context, int64, string, string) error { return nil }
219
220func (*fakeBridgeService) DeleteFolder(context.Context, int64) error { return nil }
221
222func (*fakeBridgeService) CreateTag(context.Context, string, string) (*entity.Tag, error) {
223 return nil, nil
224}
225
226func (*fakeBridgeService) UpdateTag(context.Context, int64, string, string) error { return nil }
227
228func (*fakeBridgeService) DeleteTag(context.Context, int64) error { return nil }
229
230func (*fakeBridgeService) AssignTag(context.Context, int64, int64) error { return nil }
231
232func (*fakeBridgeService) RemoveTag(context.Context, int64, int64) error { return nil }
233
234func (f *fakeBridgeService) Current(context.Context) (dto.SystemviewConfigPayload, error) {
235 f.calledConfig.Store(true)
236 return f.currentConfig, nil
237}
238
239func (*fakeBridgeService) Default(context.Context) (dto.SystemviewConfigPayload, error) {
240 return dto.SystemviewConfigPayload{}, nil
241}
242
243func (*fakeBridgeService) Save(context.Context, dto.WebUIConfig) error { return nil }
244
245func (f *fakeBridgeService) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
246 f.calledKeybindings.Store(true)
247 return f.keybindings, nil
248}
249
250func (*fakeBridgeService) SetKeybinding(context.Context, port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
251 return port.SetKeybindingResponse{}, nil
252}
253
254func (*fakeBridgeService) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error {
255 return nil
256}
257
258func (*fakeBridgeService) ResetAllKeybindings(context.Context) error { return nil }