v0.30.0 eb3068f

native_popup_window_test.go RawHistoryFolder

1package ui
2
3import (
4	"context"
5	"errors"
6	"testing"
7	"time"
8
9	"github.com/bnema/dumber/internal/application/port"
10	portmocks "github.com/bnema/dumber/internal/application/port/mocks"
11	"github.com/bnema/dumber/internal/shared/syncdispatch"
12	"github.com/bnema/dumber/internal/ui/coordinator/content"
13	layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks"
14	"github.com/bnema/dumber/internal/ui/window"
15	"github.com/bnema/puregotk/v4/gtk"
16	"github.com/stretchr/testify/assert"
17	"github.com/stretchr/testify/require"
18)
19
20type popupDestroySpy struct {
21	destroyed bool
22}
23
24func (p *popupDestroySpy) Destroy() {
25	p.destroyed = true
26}
27
28func TestPrepareNativePopupContentWidget_ExpandsWrappedWidget(t *testing.T) {
29	widget := layoutmocks.NewMockWidget(t)
30	gtkWidget := &gtk.Widget{}
31	widget.EXPECT().GtkWidget().Return(gtkWidget).Once()
32	widget.EXPECT().SetHexpand(true).Once()
33	widget.EXPECT().SetVexpand(true).Once()
34
35	got, err := prepareNativePopupContentWidget(widget)
36	require.NoError(t, err)
37	assert.Same(t, gtkWidget, got)
38}
39
40func TestPrepareNativePopupContentWidget_ErrorsWhenWrappedGTKWidgetMissing(t *testing.T) {
41	widget := layoutmocks.NewMockWidget(t)
42	widget.EXPECT().GtkWidget().Return(nil).Once()
43
44	got, err := prepareNativePopupContentWidget(widget)
45	require.Error(t, err)
46	assert.Nil(t, got)
47}
48
49func TestOpenNativePopupWindow_DestroysWebViewWhenPopupShellCreationFails(t *testing.T) {
50	oldNewPopupWindow := newPopupWindow
51	t.Cleanup(func() {
52		newPopupWindow = oldNewPopupWindow
53	})
54
55	expectedErr := errors.New("boom")
56	newPopupWindow = func(context.Context, *gtk.Application) (*window.PopupWindow, error) {
57		return nil, expectedErr
58	}
59
60	wv := portmocks.NewMockWebView(t)
61	wv.EXPECT().IsDestroyed().Return(false).Once()
62	wv.EXPECT().Destroy().Once()
63
64	app := &App{gtkApp: &gtk.Application{}}
65	err := app.openNativePopupWindow(context.Background(), content.NativePopupInput{PopupWebView: wv})
66	require.ErrorIs(t, err, expectedErr)
67}
68
69func TestDestroyFailedNativePopupSetup_DestroysWebViewAndShell(t *testing.T) {
70	wv := portmocks.NewMockWebView(t)
71	wv.EXPECT().IsDestroyed().Return(false).Once()
72	wv.EXPECT().Destroy().Once()
73
74	shell := &popupDestroySpy{}
75	destroyFailedNativePopupSetup(shell, wv)
76
77	assert.True(t, shell.destroyed)
78}
79
80func TestDispatchNativePopupLifecycleSkipsWorkWhenDispatchTimesOutBeforeStart(t *testing.T) {
81	var ran bool
82	app := &App{
83		dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
84			return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchTimedOut, Elapsed: 5 * time.Millisecond}
85		},
86	}
87
88	app.dispatchNativePopupLifecycle("ui.native_popup.close", port.WebViewID(99), func() { ran = true })
89
90	assert.False(t, ran)
91}
92
93func TestDispatchNativePopupLifecycleTreatsCompletedAfterTimeoutAsCompleted(t *testing.T) {
94	var ran bool
95	app := &App{
96		dispatchOnMainThread: func(label string, fn func()) syncdispatch.SyncDispatchResult {
97			fn()
98			return syncdispatch.SyncDispatchResult{Label: label, Status: syncdispatch.SyncDispatchCompletedAfterTimeout, Elapsed: 10 * time.Millisecond}
99		},
100	}
101
102	app.dispatchNativePopupLifecycle("ui.native_popup.close", port.WebViewID(99), func() { ran = true })
103
104	assert.True(t, ran)
105}
106
107func TestReleaseNativePopupWindow_DestroysWebViewAndRemovesState(t *testing.T) {
108	wv := portmocks.NewMockWebView(t)
109	wv.EXPECT().IsDestroyed().Return(false).Once()
110	wv.EXPECT().Destroy().Once()
111
112	app := &App{nativePopupWindows: map[port.WebViewID]*nativePopupWindow{
113		port.WebViewID(1): {
114			popupID: port.WebViewID(1),
115			webView: wv,
116		},
117	}}
118
119	app.releaseNativePopupWindow(port.WebViewID(1), false, false)
120	_, ok := app.nativePopupWindows[port.WebViewID(1)]
121	assert.False(t, ok)
122}

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