main 5041353

split_notify_cleanup_test.go RawHistoryFolder

1package layout
2
3import (
4	"context"
5	"sync/atomic"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/require"
10
11	"github.com/bnema/puregotk/v4/glib"
12)
13
14// notifyingPaned only implements the SplitView interactions exercised below.
15// Embedding the interface supplies the unused methods.
16type notifyingPaned struct {
17	PanedWidget
18	position int
19	notify   func()
20}
21
22func (p *notifyingPaned) SetResizeStartChild(bool)             {}
23func (p *notifyingPaned) SetResizeEndChild(bool)               {}
24func (p *notifyingPaned) SetVisible(bool)                      {}
25func (p *notifyingPaned) ConnectNotifyPosition(fn func()) uint { p.notify = fn; return 0 }
26func (p *notifyingPaned) GetAllocatedWidth() int               { return 100 }
27func (p *notifyingPaned) GetPosition() int                     { return p.position }
28func (p *notifyingPaned) SetPosition(position int)             { p.position = position }
29
30type notifyingFactory struct {
31	WidgetFactory
32	paned PanedWidget
33}
34
35func (f notifyingFactory) NewPaned(Orientation) PanedWidget { return f.paned }
36
37func newNotifyingSplitView(t *testing.T) (*SplitView, func(int)) {
38	t.Helper()
39
40	paned := &notifyingPaned{}
41	sv := NewSplitView(context.Background(), notifyingFactory{paned: paned}, OrientationHorizontal, nil, nil, 0)
42	sv.mu.Lock()
43	sv.suppressNotifyUntil = time.Time{}
44	sv.mu.Unlock()
45	require.NotNil(t, paned.notify)
46	return sv, func(nextPosition int) {
47		paned.position = nextPosition
48		paned.notify()
49	}
50}
51
52func receiveQueuedIdle(t *testing.T, queued <-chan *glib.SourceFunc) *glib.SourceFunc {
53	t.Helper()
54	select {
55	case source := <-queued:
56		return source
57	case <-time.After(time.Second):
58		t.Fatal("timed out waiting for debounce to queue idle callback")
59		return nil
60	}
61}
62
63func setIdleScheduler(sv *SplitView, queued chan<- *glib.SourceFunc) {
64	sv.idleAdd = func(source *glib.SourceFunc, _ uintptr) uint {
65		queued <- source
66		return 1
67	}
68}
69
70func TestSplitViewQueuedRatioCallbackDoesNotRunAfterCleanup(t *testing.T) {
71	queued := make(chan *glib.SourceFunc, 1)
72	sv, notify := newNotifyingSplitView(t)
73	setIdleScheduler(sv, queued)
74	var calls atomic.Int32
75	sv.SetOnRatioChanged(func(float64) { calls.Add(1) })
76
77	notify(25)
78	source := receiveQueuedIdle(t, queued) // Debounce has fired and queued this work.
79	sv.Cleanup()
80	(*source)(0) // Run the queued GLib closure after cleanup.
81
82	require.Zero(t, calls.Load(), "cleanup must invalidate queued ratio callbacks")
83}
84
85func TestSplitViewSupersededRatioCallbackDeliversOnlyLatestValueOnce(t *testing.T) {
86	queued := make(chan *glib.SourceFunc, 2)
87	sv, notify := newNotifyingSplitView(t)
88	setIdleScheduler(sv, queued)
89	var ratios []float64
90	sv.SetOnRatioChanged(func(ratio float64) { ratios = append(ratios, ratio) })
91
92	notify(20)
93	first := receiveQueuedIdle(t, queued)
94	notify(80)
95	second := receiveQueuedIdle(t, queued)
96	(*first)(0)
97	(*second)(0)
98
99	require.Equal(t, []float64{0.8}, ratios)
100}
101
102func TestSplitViewIdleSchedulersAreInstanceScoped(t *testing.T) {
103	firstQueued := make(chan *glib.SourceFunc, 1)
104	secondQueued := make(chan *glib.SourceFunc, 1)
105	first, firstNotify := newNotifyingSplitView(t)
106	second, secondNotify := newNotifyingSplitView(t)
107	setIdleScheduler(first, firstQueued)
108	setIdleScheduler(second, secondQueued)
109
110	firstNotify(20)
111	secondNotify(80)
112
113	firstSource := receiveQueuedIdle(t, firstQueued)
114	secondSource := receiveQueuedIdle(t, secondQueued)
115	var firstCalls, secondCalls atomic.Int32
116	first.SetOnRatioChanged(func(float64) { firstCalls.Add(1) })
117	second.SetOnRatioChanged(func(float64) { secondCalls.Add(1) })
118	(*firstSource)(0)
119	(*secondSource)(0)
120
121	require.Equal(t, int32(1), firstCalls.Load())
122	require.Equal(t, int32(1), secondCalls.Load())
123}

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