1package ui
2
3import (
4 "testing"
5
6 "github.com/bnema/puregotk/v4/glib"
7 "github.com/bnema/puregotk/v4/gobject"
8 "github.com/bnema/puregotk/v4/gtk"
9 "github.com/stretchr/testify/require"
10)
11
12// watchFinalized reports, through the returned flag, when widget is finalized.
13func watchFinalized(t *testing.T, widget *gtk.Widget) *bool {
14 t.Helper()
15 finalized := new(bool)
16 notify := gobject.WeakNotify(func(_, _ uintptr) { *finalized = true })
17 t.Cleanup(func() { _ = notify })
18 widget.WeakRef(¬ify, 0)
19 return finalized
20}
21
22func TestGTKModeToasterViewReleasesWidgets(t *testing.T) {
23 gtkApp := requireGTKDisplayApp(t)
24 defer gtkApp.Unref()
25 // Windows may only be created once the GApplication has started up;
26 // registering emits the startup signal.
27 registered, err := gtkApp.Register(nil)
28 require.NoError(t, err)
29 require.True(t, registered, "the test application registers")
30
31 var view *gtkModeToasterView
32 done := make(chan struct{})
33 run := glib.SourceFunc(func(uintptr) bool {
34 defer close(done)
35 window := gtk.NewApplicationWindow(gtkApp)
36 require.NotNil(t, window)
37 overlay := gtk.NewOverlay()
38 window.SetChild(&overlay.Widget)
39 overlayFinalized := watchFinalized(t, &overlay.Widget)
40 view = newGTKModeToasterView(overlay, window, modeToasterEvents{})
41 require.NotNil(t, view)
42
43 groups := []legendGroup{{Name: "SCROLL", Rows: []legendRow{{Action: "down", Keys: []string{"j"}, Desc: "Down"}}}}
44 view.setLegend(groups)
45 row := watchFinalized(t, view.rows["down"][0])
46 keycap := watchFinalized(t, view.keycaps["down"][0])
47 group := watchFinalized(t, &view.groupBoxes[0].Widget)
48
49 view.setLegend(nil)
50 require.True(t, *row, "a replaced legend row is released")
51 require.True(t, *keycap, "a replaced legend keycap is released")
52 require.True(t, *group, "a replaced legend group is released")
53
54 view.setLegend(groups)
55 view.layoutColumns(1)
56 column := watchFinalized(t, &view.columnBoxes[0].Widget)
57 view.layoutColumns(2)
58 require.True(t, *column, "a column dropped by a relayout is released")
59
60 pill := watchFinalized(t, &view.pill.Widget)
61 body := watchFinalized(t, &view.root.Widget)
62 label := watchFinalized(t, &view.pillLabel.Widget)
63 lastRow := watchFinalized(t, view.rows["down"][0])
64 view.destroy()
65 require.True(t, *pill, "destroy releases the pill")
66 require.True(t, *body, "destroy releases the legend body")
67 require.True(t, *label, "destroy releases the pill label")
68 require.True(t, *lastRow, "destroy releases the legend rows")
69
70 frame := newModeFrame(overlay)
71 require.NotNil(t, frame)
72 border := watchFinalized(t, &frame.border.Widget)
73 frame.destroy()
74 require.True(t, *border, "destroy releases the mode frame border")
75
76 // The window is never presented: destroying an unrealized window
77 // crashes on Wayland, so drop the references instead (the same as
78 // the global shortcut handler test). The overlay's last reference
79 // goes with the window's child; the window itself stays on GTK's
80 // toplevel list until destroyed, so its finalization is not asserted.
81 window.SetChild(nil)
82 overlay.Unref()
83 require.True(t, *overlayFinalized, "the test releases its overlay")
84 gtkApp.RemoveWindow(&window.Window)
85 window.Unref()
86 return false
87 })
88 glib.MainContextDefault().Invoke(&run, 0)
89 <-done
90}