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}