browser_popup_window_test.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"errors"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/dumber/internal/application/port"
12	portmocks "github.com/bnema/dumber/internal/application/port/mocks"
13	"github.com/bnema/dumber/internal/application/usecase"
14	"github.com/bnema/dumber/internal/domain/entity"
15	"github.com/bnema/dumber/internal/ui/component"
16	"github.com/bnema/dumber/internal/ui/coordinator"
17	"github.com/bnema/dumber/internal/ui/coordinator/content"
18)
19
20func newPopupAdoptionApp(t *testing.T, attach func(context.Context, entity.TabID, *entity.Pane, port.WebView) error) (*App, *browserWindow) {
21	t.Helper()
22	bw := &browserWindow{id: "detached-window", tabs: entity.NewTabList()}
23	app := &App{
24		tabs: entity.NewTabList(), tabsUC: usecase.NewManageTabsUseCase(func() string { return "adopted-tab" }, nil),
25		browserWindows: map[string]*browserWindow{}, windowForTab: map[entity.TabID]*browserWindow{},
26		workspaceViews:       map[entity.TabID]*component.WorkspaceView{},
27		browserWindowFactory: func(context.Context, string) (*browserWindow, error) { return bw, nil },
28	}
29	app.tabCoord = coordinator.NewTabCoordinator(context.Background(), coordinator.TabCoordinatorConfig{TabsUC: app.tabsUC, Tabs: app.tabs})
30	app.tabCoord.SetOnAttachPopupToTab(attach)
31	app.tabCoord.SetOnTabClosed(func(_ context.Context, _ coordinator.TabTarget, tab *entity.Tab) {
32		delete(app.windowForTab, tab.ID)
33		delete(app.workspaceViews, tab.ID)
34		app.tabs.Remove(tab.ID)
35	})
36	app.tabCoord.SetOnCurrentWindowEmpty(func(context.Context, coordinator.TabTarget) {
37		app.cleanupEjectTargetWindow(bw)
38	})
39	return app, bw
40}
41
42type popupLifecycleWebView struct {
43	*portmocks.MockWebView
44	onReady   func()
45	onClose   func()
46	showCalls int
47}
48
49func (w *popupLifecycleWebView) SetOnReadyToShow(fn func())  { w.onReady = fn }
50func (w *popupLifecycleWebView) SetOnClose(fn func())        { w.onClose = fn }
51func (w *popupLifecycleWebView) Show()                       { w.showCalls++ }
52func (w *popupLifecycleWebView) PrimePopupNavigation(string) {}
53
54func TestOpenPopupBrowserWindowAdoptsProvidedWebView(t *testing.T) {
55	wv := portmocks.NewMockWebView(t)
56	var adopted port.WebView
57	app, bw := newPopupAdoptionApp(t, func(_ context.Context, _ entity.TabID, _ *entity.Pane, got port.WebView) error {
58		adopted = got
59		return nil
60	})
61
62	result, err := app.openPopupBrowserWindow(context.Background(), content.BrowserWindowInput{
63		PopupPane: entity.NewPane("popup-pane"), PopupWebView: wv, TargetURI: "https://example.com", Ready: true,
64	})
65
66	require.NoError(t, err)
67	assert.Same(t, wv, adopted)
68	assert.Equal(t, bw.id, result.WindowID)
69	assert.Same(t, bw, app.windowForTab["adopted-tab"])
70}
71
72func TestOpenPopupBrowserWindowWaitsForReadySignalAndShowsExactlyOnce(t *testing.T) {
73	previousShow := showPopupBrowserWindow
74	showCalls := 0
75	showPopupBrowserWindow = func(*browserWindow) { showCalls++ }
76	t.Cleanup(func() { showPopupBrowserWindow = previousShow })
77
78	wv := &popupLifecycleWebView{MockWebView: portmocks.NewMockWebView(t)}
79	app, _ := newPopupAdoptionApp(t, func(context.Context, entity.TabID, *entity.Pane, port.WebView) error { return nil })
80
81	_, err := app.openPopupBrowserWindow(context.Background(), content.BrowserWindowInput{
82		PopupPane: entity.NewPane("popup-pane"), PopupWebView: wv, Ready: false,
83	})
84
85	require.NoError(t, err)
86	require.NotNil(t, wv.onReady)
87	assert.NotNil(t, wv.onClose)
88	assert.Zero(t, showCalls)
89	wv.onReady()
90	wv.onReady()
91	assert.Equal(t, 1, showCalls)
92}
93
94func TestOpenPopupBrowserWindowShowsImmediatelyWhenAlreadyReady(t *testing.T) {
95	previousShow := showPopupBrowserWindow
96	showCalls := 0
97	showPopupBrowserWindow = func(*browserWindow) { showCalls++ }
98	t.Cleanup(func() { showPopupBrowserWindow = previousShow })
99
100	wv := &popupLifecycleWebView{MockWebView: portmocks.NewMockWebView(t)}
101	app, _ := newPopupAdoptionApp(t, func(context.Context, entity.TabID, *entity.Pane, port.WebView) error { return nil })
102
103	_, err := app.openPopupBrowserWindow(context.Background(), content.BrowserWindowInput{
104		PopupPane: entity.NewPane("popup-pane"), PopupWebView: wv, Ready: true,
105	})
106
107	require.NoError(t, err)
108	assert.Nil(t, wv.onReady)
109	assert.NotNil(t, wv.onClose)
110	assert.Equal(t, 1, showCalls)
111}
112
113func TestOpenPopupBrowserWindowTransferredWebViewIsNotDestroyed(t *testing.T) {
114	wv := &recordingWebView{id: 99}
115	app, _ := newPopupAdoptionApp(t, func(context.Context, entity.TabID, *entity.Pane, port.WebView) error { return nil })
116
117	_, err := app.openPopupBrowserWindow(context.Background(), content.BrowserWindowInput{
118		PopupPane: entity.NewPane("popup-pane"), PopupWebView: wv, Ready: true,
119	})
120
121	require.NoError(t, err)
122	assert.Zero(t, wv.destroyCalls)
123}
124
125func TestOpenPopupBrowserWindowCloseRequestClosesAdoptedLastTabAndWindow(t *testing.T) {
126	wv := &popupLifecycleWebView{MockWebView: portmocks.NewMockWebView(t)}
127	app, bw := newPopupAdoptionApp(t, func(context.Context, entity.TabID, *entity.Pane, port.WebView) error { return nil })
128
129	_, err := app.openPopupBrowserWindow(context.Background(), content.BrowserWindowInput{
130		PopupPane: entity.NewPane("popup-pane"), PopupWebView: wv, Ready: true,
131	})
132	require.NoError(t, err)
133	require.NotNil(t, wv.onClose)
134
135	wv.onClose()
136
137	assert.Zero(t, bw.tabs.Count())
138	assert.False(t, app.hasBrowserWindow(bw))
139	assert.Empty(t, app.windowForTab)
140}
141
142func TestOpenPopupBrowserWindowRollsBackShellWhenCreateWithPaneFails(t *testing.T) {
143	wv := portmocks.NewMockWebView(t)
144	app, bw := newPopupAdoptionApp(t, func(context.Context, entity.TabID, *entity.Pane, port.WebView) error {
145		return errors.New("attachment failed")
146	})
147
148	_, err := app.openPopupBrowserWindow(context.Background(), content.BrowserWindowInput{
149		PopupPane: entity.NewPane("popup-pane"), PopupWebView: wv, TargetURI: "https://example.com", Ready: true,
150	})
151
152	require.Error(t, err)
153	assert.False(t, app.hasBrowserWindow(bw))
154	assert.Zero(t, bw.tabs.Count())
155	assert.Empty(t, app.windowForTab)
156	assert.Empty(t, app.workspaceViews)
157}

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