返回 DeepSeek-Reasonix
fake_test.go
根目录 / internal / browser / cdp / fake_test.go
1 package cdp
2
3 import (
4 "bytes"
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "image"
10 "image/color"
11 "image/png"
12 "net/http"
13 "net/http/httptest"
14 "strings"
15 "sync"
16 "testing"
17 "time"
18
19 "github.com/gorilla/websocket"
20 )
21
22 // fakeBrowser answers the DevTools subset this package speaks. Tests script it
23 // by replacing entries in handlers or by flipping the page-script state.
24 type fakeBrowser struct {
25 t *testing.T
26 srv *httptest.Server
27
28 mu sync.Mutex
29 ws *websocket.Conn
30 calls []string
31 args map[string][]json.RawMessage
32 handlers map[string]func(params json.RawMessage) (any, *protocolError)
33 userSeq int64
34 tree string
35 refs map[string]bool
36 nextID int
37 }
38
39 func newFakeBrowser(t *testing.T) *fakeBrowser {
40 t.Helper()
41 f := &fakeBrowser{
42 t: t, args: map[string][]json.RawMessage{},
43 handlers: map[string]func(json.RawMessage) (any, *protocolError){},
44 tree: "- button \"Save\" [ref=e1]",
45 refs: map[string]bool{"e1": true},
46 }
47 mux := http.NewServeMux()
48 mux.HandleFunc("/json/version", func(w http.ResponseWriter, r *http.Request) {
49 _ = json.NewEncoder(w).Encode(map[string]string{
50 "Browser": "HeadlessChrome/fake",
51 "webSocketDebuggerUrl": "ws" + strings.TrimPrefix(f.srv.URL, "http") + "/devtools/browser/fake",
52 })
53 })
54 mux.HandleFunc("/devtools/browser/fake", f.serveSocket)
55 f.srv = httptest.NewServer(mux)
56 t.Cleanup(f.srv.Close)
57 return f
58 }
59
60 func (f *fakeBrowser) serveSocket(w http.ResponseWriter, r *http.Request) {
61 up := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
62 ws, err := up.Upgrade(w, r, nil)
63 if err != nil {
64 return
65 }
66 f.mu.Lock()
67 f.ws = ws
68 f.mu.Unlock()
69 for {
70 _, data, err := ws.ReadMessage()
71 if err != nil {
72 return
73 }
74 var msg message
75 if err := json.Unmarshal(data, &msg); err != nil {
76 continue
77 }
78 f.serve(ws, msg)
79 }
80 }
81
82 func (f *fakeBrowser) serve(ws *websocket.Conn, msg message) {
83 f.mu.Lock()
84 f.calls = append(f.calls, msg.Method)
85 f.args[msg.Method] = append(f.args[msg.Method], msg.Params)
86 handler := f.handlers[msg.Method]
87 f.mu.Unlock()
88
89 var (
90 result any
91 fail *protocolError
92 )
93 if handler != nil {
94 result, fail = handler(msg.Params)
95 } else {
96 result, fail = f.builtin(msg)
97 }
98 reply := message{ID: msg.ID, SessionID: msg.SessionID}
99 if fail != nil {
100 reply.Error = fail
101 } else {
102 raw, err := json.Marshal(result)
103 if err != nil {
104 f.t.Errorf("fake: encode %s result: %v", msg.Method, err)
105 return
106 }
107 reply.Result = raw
108 }
109 f.write(ws, reply)
110 if msg.Method == "Page.navigate" || msg.Method == "Page.reload" || msg.Method == "Page.navigateToHistoryEntry" {
111 f.emit(msg.SessionID, "Page.frameNavigated", map[string]any{"frame": map[string]any{"id": "frame-1", "url": "https://example.test/next"}})
112 f.emit(msg.SessionID, "Page.frameStoppedLoading", map[string]any{"frameId": "frame-1"})
113 }
114 }
115
116 // builtin answers the commands every test needs the same way.
117 func (f *fakeBrowser) builtin(msg message) (any, *protocolError) {
118 switch msg.Method {
119 case "Target.createTarget":
120 f.mu.Lock()
121 f.nextID++
122 id := fmt.Sprintf("target-%d", f.nextID)
123 f.mu.Unlock()
124 return map[string]any{"targetId": id}, nil
125 case "Target.attachToTarget":
126 var in struct {
127 TargetID string `json:"targetId"`
128 }
129 _ = json.Unmarshal(msg.Params, &in)
130 return map[string]any{"sessionId": "session-" + in.TargetID}, nil
131 case "Target.createBrowserContext":
132 return map[string]any{"browserContextId": "context-1"}, nil
133 case "Target.getTargetInfo":
134 return map[string]any{"targetInfo": map[string]any{"url": "https://example.test/", "title": "Example"}}, nil
135 case "Page.getFrameTree":
136 return map[string]any{"frameTree": map[string]any{"frame": map[string]any{"id": "frame-1", "url": "https://example.test/"}}}, nil
137 case "Page.createIsolatedWorld":
138 return map[string]any{"executionContextId": 7}, nil
139 case "Page.getLayoutMetrics":
140 return map[string]any{
141 "cssContentSize": map[string]any{"width": 800, "height": 2400},
142 "cssVisualViewport": map[string]any{"clientWidth": 800, "clientHeight": 600, "pageX": 0, "pageY": 0},
143 }, nil
144 case "Page.getNavigationHistory":
145 return map[string]any{"currentIndex": 1, "entries": []map[string]any{{"id": 10}, {"id": 11}}}, nil
146 case "Page.captureScreenshot":
147 return map[string]any{"data": fakePNG(f.t)}, nil
148 case "Runtime.evaluate":
149 return f.evaluate(msg.Params)
150 }
151 return map[string]any{}, nil
152 }
153
154 // evaluate stands in for the isolated world: it recognises each __rx call the
155 // executor makes and answers with the state the test set up.
156 func (f *fakeBrowser) evaluate(params json.RawMessage) (any, *protocolError) {
157 var in struct {
158 Expression string `json:"expression"`
159 ReturnByValue bool `json:"returnByValue"`
160 }
161 _ = json.Unmarshal(params, &in)
162 f.mu.Lock()
163 defer f.mu.Unlock()
164 expr := in.Expression
165 switch {
166 case strings.HasPrefix(expr, "__rx.state()"):
167 return value(map[string]any{"userSeq": f.userSeq, "url": "https://example.test/", "title": "Example", "ready": "complete"}), nil
168 case strings.HasPrefix(expr, "__rx.snapshot("):
169 return value(map[string]any{
170 "url": "https://example.test/", "title": "Example", "tree": f.tree,
171 "refs": len(f.refs), "userSeq": f.userSeq, "truncated": false,
172 }), nil
173 case strings.HasPrefix(expr, "__rx.window("):
174 return value(f.userSeq), nil
175 case strings.HasPrefix(expr, "__rx.rect("):
176 if !f.refs[refArg(expr)] {
177 return value(nil), nil
178 }
179 return value(map[string]any{"x": 40, "y": 60, "width": 80, "height": 20, "tag": "button"}), nil
180 case strings.HasPrefix(expr, "__rx.focus("):
181 return value(map[string]any{"ok": f.refs[refArg(expr)], "reason": "the element refused focus"}), nil
182 case strings.HasPrefix(expr, "__rx.select("):
183 return value(map[string]any{"ok": true, "selected": []string{"one"}}), nil
184 case strings.HasPrefix(expr, "__rx.element("):
185 if !f.refs[refArg(expr)] {
186 return map[string]any{"result": map[string]any{"type": "object", "subtype": "null"}}, nil
187 }
188 return map[string]any{"result": map[string]any{"type": "object", "objectId": "object-1"}}, nil
189 }
190 return map[string]any{"result": map[string]any{"type": "undefined"}}, nil
191 }
192
193 func value(v any) map[string]any {
194 return map[string]any{"result": map[string]any{"type": "object", "value": v}}
195 }
196
197 // refArg pulls the quoted ref out of an __rx call such as __rx.rect("e1").
198 func refArg(expr string) string {
199 _, rest, ok := strings.Cut(expr, `"`)
200 if !ok {
201 return ""
202 }
203 ref, _, ok := strings.Cut(rest, `"`)
204 if !ok {
205 return ""
206 }
207 return ref
208 }
209
210 func (f *fakeBrowser) write(ws *websocket.Conn, msg message) {
211 data, err := json.Marshal(msg)
212 if err != nil {
213 f.t.Errorf("fake: encode reply: %v", err)
214 return
215 }
216 f.mu.Lock()
217 defer f.mu.Unlock()
218 if err := ws.WriteMessage(websocket.TextMessage, data); err != nil {
219 return
220 }
221 }
222
223 func (f *fakeBrowser) emit(session, method string, params any) {
224 raw, err := json.Marshal(params)
225 if err != nil {
226 f.t.Errorf("fake: encode %s params: %v", method, err)
227 return
228 }
229 f.mu.Lock()
230 ws := f.ws
231 f.mu.Unlock()
232 if ws == nil {
233 return
234 }
235 f.write(ws, message{Method: method, Params: raw, SessionID: session})
236 }
237
238 // setHandler overrides one command for the rest of the test.
239 func (f *fakeBrowser) setHandler(method string, fn func(json.RawMessage) (any, *protocolError)) {
240 f.mu.Lock()
241 f.handlers[method] = fn
242 f.mu.Unlock()
243 }
244
245 func (f *fakeBrowser) takeOver() {
246 f.mu.Lock()
247 f.userSeq++
248 f.mu.Unlock()
249 }
250
251 func (f *fakeBrowser) countCalls(method string) int {
252 f.mu.Lock()
253 defer f.mu.Unlock()
254 n := 0
255 for _, call := range f.calls {
256 if call == method {
257 n++
258 }
259 }
260 return n
261 }
262
263 func (f *fakeBrowser) lastArgs(method string) json.RawMessage {
264 f.mu.Lock()
265 defer f.mu.Unlock()
266 list := f.args[method]
267 if len(list) == 0 {
268 return nil
269 }
270 return list[len(list)-1]
271 }
272
273 func fakePNG(t *testing.T) string {
274 t.Helper()
275 img := image.NewRGBA(image.Rect(0, 0, 4, 3))
276 img.Set(0, 0, color.RGBA{R: 255, A: 255})
277 var buf bytes.Buffer
278 if err := png.Encode(&buf, img); err != nil {
279 t.Fatalf("encode fake png: %v", err)
280 }
281 return base64.StdEncoding.EncodeToString(buf.Bytes())
282 }
283
284 // newTestExecutor attaches an executor to the fake browser with artifacts in
285 // the test's own directory.
286 func newTestExecutor(t *testing.T, f *fakeBrowser) *Executor {
287 t.Helper()
288 return newTestExecutorWithRoots(t, f)
289 }
290
291 // newTestExecutorWithRoots is newTestExecutor with the directories
292 // browser_upload may read from.
293 func newTestExecutorWithRoots(t *testing.T, f *fakeBrowser, uploadRoots ...string) *Executor {
294 t.Helper()
295 ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
296 defer cancel()
297 exec, err := New(ctx, Options{
298 Endpoint: f.srv.URL, ArtifactDir: t.TempDir(),
299 NavigateTimeout: 5 * time.Second, UploadRoots: uploadRoots,
300 })
301 if err != nil {
302 t.Fatalf("new executor: %v", err)
303 }
304 t.Cleanup(exec.Shutdown)
305 return exec
306 }
307
307 lines GO