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 := ¬ifyingPaned{}
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}