v0.29.0 21b3150

bootstrap_test.go RawHistoryFolder

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 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close