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}