v0.32.0 0617278

injection_event_test.go RawHistoryFolder

1package cef
2
3import (
4	"context"
5	"sync"
6	"testing"
7
8	purecef "github.com/bnema/purego-cef/cef"
9	cefmocks "github.com/bnema/purego-cef/cef/mocks"
10	"github.com/stretchr/testify/mock"
11	"github.com/stretchr/testify/require"
12)
13
14// injectionHarness builds a WebView with a recording main frame. Engine is
15// nil so RunJavaScript executes synchronously without GTK dispatch.
16type injectionHarness struct {
17	wv      *WebView
18	browser *cefmocks.MockBrowser
19	mu      sync.Mutex
20	scripts []string
21}
22
23func newInjectionHarness(t *testing.T, uri string) *injectionHarness {
24	t.Helper()
25	return newInjectionHarnessWithID(t, uri, 7)
26}
27
28func newInjectionHarnessWithID(t *testing.T, uri string, browserID int32) *injectionHarness {
29	t.Helper()
30	h := &injectionHarness{}
31	frame := cefmocks.NewMockFrame(t)
32	frame.EXPECT().ExecuteJavaScript(mock.Anything, "", int32(0)).Run(func(script string, _ string, _ int32) {
33		h.mu.Lock()
34		defer h.mu.Unlock()
35		h.scripts = append(h.scripts, script)
36	}).Maybe()
37	h.browser = cefmocks.NewMockBrowser(t)
38	h.browser.EXPECT().GetMainFrame().Return(frame).Maybe()
39	h.browser.EXPECT().GetIdentifier().Return(browserID).Maybe()
40	h.wv = &WebView{ctx: context.Background(), browser: h.browser, documentSeq: 1}
41	h.wv.uri = uri
42	return h
43}
44
45func (h *injectionHarness) scriptCount() int {
46	h.mu.Lock()
47	defer h.mu.Unlock()
48	return len(h.scripts)
49}
50
51func testInjector() *contentInjector {
52	return &contentInjector{themeCSS: ":root{--t:1}", findHighlightCSS: ".find{--f:1}"}
53}
54
55// TestInjectionEvent_ExternalPageScriptSet counts the installation for an
56// external document: scrollbar styling, its auto-hide behavior, the
57// clipboard bridge, plus configured find CSS. No dark-mode or message
58// bridge shim belongs to external pages.
59func TestInjectionEvent_ExternalPageScriptSet(t *testing.T) {
60	h := newInjectionHarness(t, "https://example.com/page")
61	testInjector().onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, h.wv.uri))
62
63	require.Equal(t, 4, h.scriptCount(), "external install must stay a fixed small script set")
64	h.mu.Lock()
65	defer h.mu.Unlock()
66	joined := ""
67	for _, script := range h.scripts {
68		joined += script
69	}
70	require.Contains(t, joined, "dumber-scrollbar")
71	require.Contains(t, joined, "dumber-find-highlight")
72	require.NotContains(t, joined, "dumber-theme-vars")
73	require.NotContains(t, joined, "__dumber_cef_prefers_dark")
74}
75
76// TestInjectionEvent_InternalPageScriptSet counts the installation for an
77// internal document, including dark-mode handling and the message bridge.
78func TestInjectionEvent_InternalPageScriptSet(t *testing.T) {
79	h := newInjectionHarness(t, "dumb://home")
80	testInjector().onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, h.wv.uri))
81
82	require.Equal(t, 7, h.scriptCount(), "internal install must stay a fixed small script set")
83}
84
85// TestInjectionEvent_StaleBrowserSkipped reproduces an old main frame firing
86// during process swap: the GTK-dispatched callback must install nothing.
87func TestInjectionEvent_StaleBrowserSkipped(t *testing.T) {
88	h := newInjectionHarnessWithID(t, "https://example.com/a", 7)
89	event := h.wv.captureInjectionEvent(h.browser, h.wv.uri)
90
91	replacement := cefmocks.NewMockBrowser(t)
92	replacement.EXPECT().GetIdentifier().Return(int32(8)).Maybe()
93	h.wv.mu.Lock()
94	h.wv.browser = replacement
95	h.wv.mu.Unlock()
96
97	testInjector().onLoadEndForEvent(h.wv, event)
98	require.Zero(t, h.scriptCount(), "old-frame event must not install against the replacement browser")
99}
100
101// TestInjectionEvent_StaleIntentSkipped reproduces a queued callback
102// outlived by a newer navigation: installation must not run for the
103// superseded intent.
104func TestInjectionEvent_StaleIntentSkipped(t *testing.T) {
105	h := newInjectionHarness(t, "https://example.com/a")
106	event := h.wv.captureInjectionEvent(h.browser, h.wv.uri)
107
108	h.wv.mu.Lock()
109	h.wv.pendingIntentID++
110	h.wv.mu.Unlock()
111
112	testInjector().onLoadEndForEvent(h.wv, event)
113	require.Zero(t, h.scriptCount(), "superseded intent must not install")
114}
115
116// TestInjectionEvent_CurrentInstalls verifies matching identity installs once
117// for a committed document. Repeated load-end notifications for that document
118// must not repeat the GTK-to-CEF script round trips.
119func TestInjectionEvent_CurrentInstalls(t *testing.T) {
120	h := newInjectionHarness(t, "https://example.com/a")
121	ci := testInjector()
122
123	ci.onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, h.wv.uri))
124	first := h.scriptCount()
125	require.Positive(t, first)
126
127	ci.onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, h.wv.uri))
128	require.Equal(t, first, h.scriptCount(), "repeated same-document events must be coalesced")
129
130	h.wv.mu.Lock()
131	h.wv.documentSeq++
132	h.wv.mu.Unlock()
133	ci.onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, h.wv.uri))
134	require.Equal(t, 2*first, h.scriptCount(), "a new committed document must receive scripts")
135}
136
137// TestInjectionEvent_ReplacedDocumentSkipsQueuedScripts reproduces a replacement
138// navigation committing between validation and the CEF UI thread: scripts
139// pinned to the validated document must be dropped instead of landing in the
140// new document.
141func TestInjectionEvent_ReplacedDocumentSkipsQueuedScripts(t *testing.T) {
142	h := newInjectionHarness(t, "https://example.com/a")
143
144	var queued []purecef.Task
145	prevNewTask, prevPostTask := cefNewTask, cefPostTask
146	cefNewTask = func(task purecef.Task) purecef.Task { return task }
147	cefPostTask = func(_ purecef.ThreadID, task purecef.Task) int32 {
148		queued = append(queued, task)
149		return 1
150	}
151	t.Cleanup(func() { cefNewTask, cefPostTask = prevNewTask, prevPostTask })
152
153	// A non-nil engine queues scripts for the CEF UI thread instead of running
154	// them inline, which is what opens the window the pin has to close.
155	h.wv.engine = &Engine{}
156	testInjector().onLoadEndForEvent(h.wv, h.wv.captureInjectionEvent(h.browser, h.wv.uri))
157	require.NotEmpty(t, queued, "a validated load-end must queue its installation")
158
159	// The replacement document commits before the queued scripts run.
160	h.wv.mu.Lock()
161	h.wv.documentSeq++
162	h.wv.mu.Unlock()
163	for _, task := range queued {
164		task.Execute()
165	}
166
167	require.Zero(t, h.scriptCount(), "scripts pinned to a replaced document must not install")
168}
169
170// TestInjectionEvent_NilCaptureSkips verifies the conservative fallback: a
171// capture from a nil callback browser cannot prove identity, so it installs
172// nothing; the current browser gets its own load-end installation.
173func TestInjectionEvent_NilCaptureSkips(t *testing.T) {
174	h := newInjectionHarness(t, "https://example.com/a")
175	event := h.wv.captureInjectionEvent(nil, h.wv.uri)
176	require.Equal(t, noBrowserID, event.browserID)
177
178	testInjector().onLoadEndForEvent(h.wv, event)
179	require.Zero(t, h.scriptCount(), "identity-less capture must not install blind")
180}
181
182// TestInjectionEvent_NilCurrentBrowserSkips verifies dispatch against a
183// destroyed browser installs nothing instead of dereferencing it.
184func TestInjectionEvent_NilCurrentBrowserSkips(t *testing.T) {
185	h := newInjectionHarness(t, "https://example.com/a")
186	event := h.wv.captureInjectionEvent(h.browser, h.wv.uri)
187
188	h.wv.mu.Lock()
189	h.wv.browser = nil
190	h.wv.mu.Unlock()
191	testInjector().onLoadEndForEvent(h.wv, event)
192	require.Zero(t, h.scriptCount())
193}
194
195// TestInjectionEvent_StaleCallbackBrowserSkipped reproduces the exact
196// reviewer scenario: WebView state advances to a replacement browser, then
197// the OLD callback browser fires. Identity must come from the callback, so
198// the event keeps the old identifier and is rejected. Stamping the current
199// wv.browser instead would bless it with the replacement identity.
200func TestInjectionEvent_StaleCallbackBrowserSkipped(t *testing.T) {
201	h := newInjectionHarnessWithID(t, "https://example.com/a", 7)
202	oldCallbackBrowser := h.browser
203
204	replacement := cefmocks.NewMockBrowser(t)
205	replacement.EXPECT().GetIdentifier().Return(int32(8)).Maybe()
206	h.wv.mu.Lock()
207	h.wv.browser = replacement
208	h.wv.mu.Unlock()
209
210	event := h.wv.captureInjectionEvent(oldCallbackBrowser, h.wv.uri)
211	require.Equal(t, int32(7), event.browserID, "callback-sourced capture must keep the old identity")
212	testInjector().onLoadEndForEvent(h.wv, event)
213	require.Zero(t, h.scriptCount(), "old-callback event must not install against the replacement browser")
214}

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