renderer_bridge_test.go RawHistoryFolder

1package cef
2
3import (
4	"go/ast"
5	"go/parser"
6	"go/token"
7	"os"
8	"strings"
9	"testing"
10
11	purecef "github.com/bnema/purego-cef/cef"
12	cefmocks "github.com/bnema/purego-cef/cef/mocks"
13	"github.com/stretchr/testify/mock"
14	"github.com/stretchr/testify/require"
15)
16
17type testBridgeProcessMessage struct {
18	name string
19	args *testBridgeListValue
20}
21
22type testBridgeListValue struct {
23	values map[int]string
24}
25
26func newTestBridgeProcessMessage(name string, editable bool) *testBridgeProcessMessage {
27	msg := &testBridgeProcessMessage{name: name, args: &testBridgeListValue{values: map[int]string{}}}
28	msg.args.SetString(0, rendererBridgeActionEditableFocusChanged)
29	if editable {
30		msg.args.SetString(1, "1")
31	} else {
32		msg.args.SetString(1, "0")
33	}
34	return msg
35}
36
37func (m *testBridgeProcessMessage) IsValid() bool                                     { return true }
38func (m *testBridgeProcessMessage) IsReadOnly() bool                                  { return false }
39func (m *testBridgeProcessMessage) Copy() purecef.ProcessMessage                      { return m }
40func (m *testBridgeProcessMessage) GetName() string                                   { return m.name }
41func (m *testBridgeProcessMessage) GetArgumentList() purecef.ListValue                { return m.args }
42func (m *testBridgeProcessMessage) GetSharedMemoryRegion() purecef.SharedMemoryRegion { return nil }
43
44func (v *testBridgeListValue) IsValid() bool                      { return true }
45func (v *testBridgeListValue) IsOwned() bool                      { return true }
46func (v *testBridgeListValue) IsReadOnly() bool                   { return false }
47func (v *testBridgeListValue) IsSame(that purecef.ListValue) bool { return v == that }
48func (v *testBridgeListValue) IsEqual(that purecef.ListValue) bool {
49	return v == that
50}
51func (v *testBridgeListValue) Copy() purecef.ListValue         { return v }
52func (v *testBridgeListValue) SetSize(_ int) int32             { return 0 }
53func (v *testBridgeListValue) GetSize() int                    { return len(v.values) }
54func (v *testBridgeListValue) Clear() int32                    { v.values = map[int]string{}; return 0 }
55func (v *testBridgeListValue) Remove(index int) int32          { delete(v.values, index); return 0 }
56func (v *testBridgeListValue) GetType(_ int) purecef.ValueType { return 0 }
57func (v *testBridgeListValue) GetValue(_ int) purecef.Value    { return nil }
58func (v *testBridgeListValue) GetBool(_ int) int32             { return 0 }
59func (v *testBridgeListValue) GetInt(_ int) int32              { return 0 }
60func (v *testBridgeListValue) GetDouble(_ int) float64         { return 0 }
61func (v *testBridgeListValue) GetString(index int) string {
62	if v.values == nil {
63		return ""
64	}
65	return v.values[index]
66}
67func (v *testBridgeListValue) GetBinary(_ int) purecef.BinaryValue         { return nil }
68func (v *testBridgeListValue) GetDictionary(_ int) purecef.DictionaryValue { return nil }
69func (v *testBridgeListValue) GetList(_ int) purecef.ListValue             { return nil }
70func (v *testBridgeListValue) SetValue(_ int, _ purecef.Value) int32       { return 0 }
71func (v *testBridgeListValue) SetNull(_ int) int32                         { return 0 }
72func (v *testBridgeListValue) SetBool(_ int, _ int32) int32                { return 0 }
73func (v *testBridgeListValue) SetInt(_ int, _ int32) int32                 { return 0 }
74func (v *testBridgeListValue) SetDouble(_ int, _ float64) int32            { return 0 }
75func (v *testBridgeListValue) SetString(index int, value string) int32 {
76	if v.values == nil {
77		v.values = map[int]string{}
78	}
79	v.values[index] = value
80	return 0
81}
82func (v *testBridgeListValue) SetBinary(_ int, _ purecef.BinaryValue) int32         { return 0 }
83func (v *testBridgeListValue) SetDictionary(_ int, _ purecef.DictionaryValue) int32 { return 0 }
84func (v *testBridgeListValue) SetList(_ int, _ purecef.ListValue) int32             { return 0 }
85
86func TestRendererBridgeExtensionJS_DoesNotExposeWritableGlobalDispatch(t *testing.T) {
87	require.NotContains(t, rendererBridgeExtensionJS, "window.__dumberNativeBridge")
88	require.NotContains(t, rendererBridgeExtensionJS, "DumberBridgeDispatch =")
89	require.NotContains(t, rendererBridgeExtensionJS, "window.DumberBridgeDispatch")
90	require.NotContains(t, rendererBridgeExtensionJS, "window.__dumberBridgeAction")
91	require.NotContains(t, rendererBridgeExtensionJS, "window.__dumberBridgePayload")
92}
93
94func TestRendererBridgeExtensionJS_UsesNativeDispatchInExtensionScope(t *testing.T) {
95	require.Contains(t, rendererBridgeExtensionJS, "native function Dispatch(action, payload);")
96	require.Contains(t, rendererBridgeExtensionJS, "return Dispatch(action, payload == null ? '' : String(payload));")
97	require.Contains(t, rendererBridgeExtensionJS, "send('bridge_ready',")
98}
99
100func TestRendererBridgeExtensionJS_DoesNotShimWindowOpenForSyntheticPopups(t *testing.T) {
101	require.NotContains(t, rendererBridgeExtensionJS, "window.__dumberPopupOpenPatched")
102	require.NotContains(t, rendererBridgeExtensionJS, "function dispatchPopupOpen(payload)")
103	require.NotContains(t, rendererBridgeExtensionJS, "send('popup_open', JSON.stringify(payload));")
104	require.NotContains(t, rendererBridgeExtensionJS, "dispatchPopupNavigate(proxyID, href)")
105	require.NotContains(t, rendererBridgeExtensionJS, "dispatchPopupClose(proxyID)")
106	require.NotContains(t, rendererBridgeExtensionJS, "return popupProxy;")
107}
108
109func TestRendererBridgeExtensionJS_EncodesTrustedSuccessSemantics(t *testing.T) {
110	require.Contains(t, rendererBridgeExtensionJS, "if (!e.isTrusted) return;")
111	require.Contains(t, rendererBridgeExtensionJS, "setTimeout(function() {")
112	require.Contains(t, rendererBridgeExtensionJS, "if (!text && !e.defaultPrevented) text = getSelectedText();")
113	require.NotContains(t, rendererBridgeExtensionJS, "document.execCommand = function")
114	require.Contains(t, rendererBridgeExtensionJS, "return result;")
115}
116
117func TestRendererBridgePackageDoesNotNeedGoLinkname(t *testing.T) {
118	files := parseCEFPackageFiles(t)
119	for _, file := range files {
120		for _, group := range file.Comments {
121			for _, comment := range group.List {
122				require.NotContains(t, comment.Text, "go:linkname")
123			}
124		}
125	}
126}
127
128func TestCEFPackageDoesNotKeepDeprecatedClipboardBridgeConstants(t *testing.T) {
129	files := parseCEFPackageFiles(t)
130	for _, banned := range []string{"autoCopySelectionBridgeJS", "clipboardCopyBridgeJS", "editableFocusBridgeJS"} {
131		require.False(t, packageDeclaresIdentifier(files, banned), "unexpected deprecated identifier %q", banned)
132	}
133}
134
135func TestDecodeRendererBridgeExplicitTextCopyPayload(t *testing.T) {
136	req, err := decodeRendererBridgeExplicitTextCopyPayload([]byte(`{"text":"copied text","action":"cut"}`))
137	require.NoError(t, err)
138	require.Equal(t, "copied text", req.Text)
139	require.Equal(t, "cut", req.Action)
140}
141
142func TestDecodeRendererBridgePopupOpenPayload(t *testing.T) {
143	req, err := decodeRendererBridgePopupOpenPayload([]byte(`{"proxy_id":"popup-1","url":"https://example.com/login","frame_name":"_blank","user_gesture":true,"no_javascript_access":true}`))
144	require.NoError(t, err)
145	require.Equal(t, "popup-1", req.ProxyID)
146	require.Equal(t, "https://example.com/login", req.URL)
147	require.Equal(t, "_blank", req.FrameName)
148	require.True(t, req.UserGesture)
149	require.True(t, req.NoJavaScriptAccess)
150}
151
152func TestDecodeRendererBridgePopupNavigatePayload(t *testing.T) {
153	req, err := decodeRendererBridgePopupNavigatePayload([]byte(`{"proxy_id":"popup-1","url":"https://example.com/callback"}`))
154	require.NoError(t, err)
155	require.Equal(t, "popup-1", req.ProxyID)
156	require.Equal(t, "https://example.com/callback", req.URL)
157}
158
159func TestDecodeRendererBridgePopupClosePayload(t *testing.T) {
160	req, err := decodeRendererBridgePopupClosePayload([]byte(`{"proxy_id":"popup-1"}`))
161	require.NoError(t, err)
162	require.Equal(t, "popup-1", req.ProxyID)
163}
164
165func TestDecodeRendererBridgePopupPayloadErrors(t *testing.T) {
166	tests := []struct {
167		name   string
168		decode func([]byte) error
169	}{
170		{
171			name: "open",
172			decode: func(payload []byte) error {
173				_, err := decodeRendererBridgePopupOpenPayload(payload)
174				return err
175			},
176		},
177		{
178			name: "navigate",
179			decode: func(payload []byte) error {
180				_, err := decodeRendererBridgePopupNavigatePayload(payload)
181				return err
182			},
183		},
184		{
185			name: "close",
186			decode: func(payload []byte) error {
187				_, err := decodeRendererBridgePopupClosePayload(payload)
188				return err
189			},
190		},
191	}
192
193	payloads := []struct {
194		name    string
195		payload []byte
196	}{
197		{name: "empty", payload: nil},
198		{name: "invalid json", payload: []byte(`{invalid`)},
199		{name: "missing proxy id", payload: []byte(`{"url":"https://example.com"}`)},
200	}
201
202	for _, test := range tests {
203		t.Run(test.name, func(t *testing.T) {
204			for _, payload := range payloads {
205				t.Run(payload.name, func(t *testing.T) {
206					require.Error(t, test.decode(payload.payload))
207				})
208			}
209		})
210	}
211}
212
213func parseCEFPackageFiles(t *testing.T) []*ast.File {
214	t.Helper()
215	entries, err := os.ReadDir(".")
216	require.NoError(t, err)
217
218	fset := token.NewFileSet()
219	files := make([]*ast.File, 0, len(entries))
220	for _, entry := range entries {
221		name := entry.Name()
222		if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") {
223			continue
224		}
225		file, err := parser.ParseFile(fset, name, nil, parser.ParseComments)
226		require.NoError(t, err)
227		if file.Name != nil && file.Name.Name == "cef" {
228			files = append(files, file)
229		}
230	}
231	require.NotEmpty(t, files)
232	return files
233}
234
235func packageDeclaresIdentifier(files []*ast.File, name string) bool {
236	for _, file := range files {
237		found := false
238		ast.Inspect(file, func(node ast.Node) bool {
239			ident, ok := node.(*ast.Ident)
240			if ok && ident.Name == name {
241				found = true
242				return false
243			}
244			return true
245		})
246		if found {
247			return true
248		}
249	}
250	return false
251}
252
253func TestRendererBridgeProcessHandler_OnFocusedNodeChanged_ReportsEditableState(t *testing.T) {
254	oldFactory := newRendererBridgeProcessMessage
255	t.Cleanup(func() { newRendererBridgeProcessMessage = oldFactory })
256	newRendererBridgeProcessMessage = func(name string) purecef.ProcessMessage {
257		return newTestBridgeProcessMessage(name, true)
258	}
259
260	frame := cefmocks.NewMockFrame(t)
261	frame.EXPECT().SendProcessMessage(purecef.ProcessIDPidBrowser, mock.Anything).Run(func(_ purecef.ProcessID, message purecef.ProcessMessage) {
262		require.Equal(t, rendererBridgeMessageName, message.GetName())
263		args := message.GetArgumentList()
264		require.Equal(t, "editable_focus_changed", args.GetString(0))
265		require.Equal(t, "1", args.GetString(1))
266	}).Once()
267
268	(&rendererBridgeProcessHandler{}).OnFocusedNodeChanged(nil, frame, stubEditableDomnode{editable: true})
269}
270
271type runtimeEditableDomnode struct {
272	stubEditableDomnode
273	tagName string
274}
275
276func (n runtimeEditableDomnode) GetElementTagName() string { return n.tagName }
277
278func TestRuntimeRenderProcessHandlers_ReportEditableFocusedNodes(t *testing.T) {
279	oldFactory := newRendererBridgeProcessMessage
280	t.Cleanup(func() { newRendererBridgeProcessMessage = oldFactory })
281	newRendererBridgeProcessMessage = func(name string) purecef.ProcessMessage {
282		return newTestBridgeProcessMessage(name, true)
283	}
284
285	apps := []struct {
286		name string
287		new  func() purecef.App
288	}{
289		{name: "main process", new: func() purecef.App { return newDumberApp(&Engine{}) }},
290		{name: "subprocess", new: NewSubprocessApp},
291	}
292	nodes := []struct {
293		name string
294		node purecef.Domnode
295	}{
296		{name: "input", node: runtimeEditableDomnode{stubEditableDomnode: stubEditableDomnode{editable: true}, tagName: "INPUT"}},
297		{name: "textarea", node: runtimeEditableDomnode{stubEditableDomnode: stubEditableDomnode{editable: true}, tagName: "TEXTAREA"}},
298		{name: "contenteditable", node: runtimeEditableDomnode{stubEditableDomnode: stubEditableDomnode{editable: true}, tagName: "DIV"}},
299	}
300
301	for _, appCase := range apps {
302		t.Run(appCase.name, func(t *testing.T) {
303			for _, nodeCase := range nodes {
304				t.Run(nodeCase.name, func(t *testing.T) {
305					frame := cefmocks.NewMockFrame(t)
306					frame.EXPECT().SendProcessMessage(purecef.ProcessIDPidBrowser, mock.Anything).
307						Run(func(_ purecef.ProcessID, message purecef.ProcessMessage) {
308							action, editable, ok := decodeRendererBridgeProcessMessage(message)
309							require.True(t, ok)
310							require.Equal(t, rendererBridgeActionEditableFocusChanged, action)
311							require.Equal(t, "1", editable)
312						}).Once()
313
314					handler := appCase.new().GetRenderProcessHandler()
315					require.NotNil(t, handler)
316					handler.OnFocusedNodeChanged(nil, frame, nodeCase.node)
317				})
318			}
319		})
320	}
321}
322
323func TestRuntimeRenderProcessHandlers_PreservePopupOpenerCallbacks(t *testing.T) {
324	apps := []struct {
325		name string
326		new  func() purecef.App
327	}{
328		{name: "main process", new: func() purecef.App { return newDumberApp(&Engine{}) }},
329		{name: "subprocess", new: NewSubprocessApp},
330	}
331
332	for _, appCase := range apps {
333		t.Run(appCase.name, func(t *testing.T) {
334			browser := cefmocks.NewMockBrowser(t)
335			browser.EXPECT().GetIdentifier().Return(int32(42)).Times(4)
336			ctx := &testPopupOpenerV8Context{valid: true}
337			handler := appCase.new().GetRenderProcessHandler()
338			require.NotNil(t, handler)
339			require.Nil(t, handler.GetLoadHandler())
340
341			handler.OnBrowserCreated(browser, testPopupOpenerDictionaryValue{
342				strings: map[string]string{
343					popupOpenerExtraInfoParentURIKey:   "https://example.com/login",
344					popupOpenerExtraInfoBridgeNonceKey: "bridge-nonce",
345				},
346				bools: map[string]int32{popupOpenerExtraInfoEnabledKey: 1},
347			})
348			handler.OnContextCreated(browser, stubFrame{main: true, url: "https://example.com/popup"}, ctx)
349
350			require.Equal(t, 1, ctx.enterCalls)
351			require.Equal(t, 1, ctx.exitCalls)
352			require.Contains(t, ctx.evalCode, "popup-opener-navigate")
353			require.Contains(t, ctx.evalCode, "popup-opener-post-message")
354			require.Equal(t, popupOpenerRenderScriptURL, ctx.evalURL)
355			require.Zero(t, handler.OnProcessMessageReceived(nil, nil, purecef.ProcessIDPidBrowser, nil))
356
357			handler.OnBrowserDestroyed(browser)
358			releasedCtx := &testPopupOpenerV8Context{valid: true}
359			handler.OnContextCreated(browser, stubFrame{main: true, url: "https://example.com/popup"}, releasedCtx)
360			require.Zero(t, releasedCtx.enterCalls)
361			require.Empty(t, releasedCtx.evalCode)
362		})
363	}
364}

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