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 := &bridgeServiceRecorder{historyEntries: []*entity.HistoryEntry{{Title: "Example", URL: "https://example.com"}}}
21 app := newBridgeApp(&recordingDOM{}, "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 := &bridgeServiceRecorder{
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 := &recordingDOM{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 := &bridgeServiceRecorder{
66 favorites: []*entity.Favorite{{Title: "Example", URL: "https://example.com"}},
67 tags: []*entity.Tag{{Name: "Go"}},
68 }
69 app := newBridgeApp(&recordingDOM{}, "dumb://favorites", bridge)
70
71 require.NoError(t, app.LoadInitial(context.Background()))
72 assert.False(t, bridge.calledHistory.Load())
73 assert.True(t, bridge.calledFavorites.Load())
74 assert.True(t, bridge.calledConfig.Load())
75 assert.False(t, bridge.calledKeybindings.Load())
76}
77
78func TestNewBridgeApp_WiresConfigService(t *testing.T) {
79 t.Parallel()
80
81 bridge := &bridgeServiceRecorder{
82 currentConfig: dto.SystemviewConfigPayload{EngineType: "webkit"},
83 keybindings: port.KeybindingsConfig{Groups: []port.KeybindingGroup{{DisplayName: "Default"}}},
84 }
85 app := newBridgeApp(&recordingDOM{}, "dumb://config", bridge)
86
87 require.NoError(t, app.LoadInitial(context.Background()))
88 assert.False(t, bridge.calledHistory.Load())
89 assert.False(t, bridge.calledFavorites.Load())
90 assert.True(t, bridge.calledConfig.Load())
91 assert.True(t, bridge.calledKeybindings.Load())
92}
93
94type recordingDOM struct {
95 html string
96 mounts chan string
97}
98
99func (f *recordingDOM) Mount(html string) error {
100 f.html = html
101 if f.mounts != nil {
102 select {
103 case f.mounts <- html:
104 default:
105 }
106 }
107 return nil
108}
109
110func receiveMountContaining(t *testing.T, mounts <-chan string, values ...string) string {
111 t.Helper()
112 deadline := time.After(time.Second)
113 for {
114 select {
115 case html := <-mounts:
116 matched := true
117 for _, value := range values {
118 if !strings.Contains(html, value) {
119 matched = false
120 break
121 }
122 }
123 if matched {
124 return html
125 }
126 case <-deadline:
127 t.Fatalf("timed out waiting for DOM mount containing %q", values)
128 return ""
129 }
130 }
131}
132
133// Recording fixture intentionally tracks state across history, favorites, config,
134// and keybindings boundaries for the composite bootstrap assertions.
135type bridgeServiceRecorder struct {
136 calledHistory atomic.Bool
137 calledFavorites atomic.Bool
138 calledConfig atomic.Bool
139 calledKeybindings atomic.Bool
140
141 historyEntries []*entity.HistoryEntry
142 favorites []*entity.Favorite
143 tags []*entity.Tag
144 currentConfig dto.SystemviewConfigPayload
145 keybindings port.KeybindingsConfig
146}
147
148func (f *bridgeServiceRecorder) Timeline(context.Context, int, int) ([]*entity.HistoryEntry, error) {
149 f.calledHistory.Store(true)
150 return f.historyEntries, nil
151}
152
153func (f *bridgeServiceRecorder) TimelineByDomain(context.Context, string, int, int) ([]*entity.HistoryEntry, error) {
154 f.calledHistory.Store(true)
155 return f.historyEntries, nil
156}
157
158func (f *bridgeServiceRecorder) TimelineWindow(_ context.Context, before time.Time, _ int64, _ string) (*entity.HistoryWindow, error) {
159 f.calledHistory.Store(true)
160 return &entity.HistoryWindow{Entries: f.historyEntries, Before: before, After: before.Add(-24 * time.Hour)}, nil
161}
162
163func (*bridgeServiceRecorder) Search(context.Context, string, int) ([]*entity.HistoryEntry, error) {
164 return nil, nil
165}
166
167func (*bridgeServiceRecorder) DeleteEntry(context.Context, int64) error { return nil }
168
169func (*bridgeServiceRecorder) DeleteRange(context.Context, string) error { return nil }
170
171func (*bridgeServiceRecorder) Stats(context.Context) (*entity.HistoryStats, error) {
172 return nil, nil
173}
174
175func (*bridgeServiceRecorder) Analytics(context.Context) (*entity.HistoryAnalytics, error) {
176 return nil, nil
177}
178
179func (*bridgeServiceRecorder) DomainStats(context.Context, int) ([]*entity.DomainStat, error) {
180 return nil, nil
181}
182
183func (*bridgeServiceRecorder) DeleteDomain(context.Context, string) error { return nil }
184
185func (f *bridgeServiceRecorder) List(context.Context) ([]*entity.Favorite, error) {
186 f.calledFavorites.Store(true)
187 return f.favorites, nil
188}
189
190func (*bridgeServiceRecorder) CreateFavorite(context.Context, dto.FavoriteCreateInput) (*entity.Favorite, error) {
191 return nil, nil
192}
193
194func (*bridgeServiceRecorder) UpdateFavorite(context.Context, dto.FavoriteUpdateInput) (*entity.Favorite, error) {
195 return nil, nil
196}
197
198func (*bridgeServiceRecorder) DeleteFavorite(context.Context, int64) error { return nil }
199
200func (f *bridgeServiceRecorder) ListTags(context.Context) ([]*entity.Tag, error) {
201 return f.tags, nil
202}
203
204func (*bridgeServiceRecorder) SetShortcut(context.Context, int64, *int) error { return nil }
205
206func (*bridgeServiceRecorder) CreateTag(context.Context, string, string) (*entity.Tag, error) {
207 return nil, nil
208}
209
210func (*bridgeServiceRecorder) UpdateTag(context.Context, int64, string, string) error { return nil }
211
212func (*bridgeServiceRecorder) DeleteTag(context.Context, int64) error { return nil }
213
214func (*bridgeServiceRecorder) AssignTag(context.Context, int64, int64) error { return nil }
215
216func (*bridgeServiceRecorder) RemoveTag(context.Context, int64, int64) error { return nil }
217
218func (f *bridgeServiceRecorder) Current(context.Context) (dto.SystemviewConfigPayload, error) {
219 f.calledConfig.Store(true)
220 return f.currentConfig, nil
221}
222
223func (*bridgeServiceRecorder) Default(context.Context) (dto.SystemviewConfigPayload, error) {
224 return dto.SystemviewConfigPayload{}, nil
225}
226
227func (*bridgeServiceRecorder) Save(context.Context, dto.WebUIConfig) error { return nil }
228
229func (f *bridgeServiceRecorder) GetKeybindings(context.Context) (port.KeybindingsConfig, error) {
230 f.calledKeybindings.Store(true)
231 return f.keybindings, nil
232}
233
234func (*bridgeServiceRecorder) SetKeybinding(context.Context, port.SetKeybindingRequest) (port.SetKeybindingResponse, error) {
235 return port.SetKeybindingResponse{}, nil
236}
237
238func (*bridgeServiceRecorder) ResetKeybinding(context.Context, port.ResetKeybindingRequest) error {
239 return nil
240}
241
242func (*bridgeServiceRecorder) ResetAllKeybindings(context.Context) error { return nil }