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 := &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 }

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