package cdp import ( "context" "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/coder/websocket" ) // tinyPNG 是 1x1 红色 PNG 的 base64(截图测试用)。 const tinyPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" // fakeCDPHandler 处理一条 CDP 命令并回响应;navigate 先发事件再回响应, // 用于验证"先订阅后触发"的事件时序。 func fakeCDPHandler(t *testing.T, title string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { return } defer c.Close(websocket.StatusInternalError, "") ctx := r.Context() for { _, data, err := c.Read(ctx) if err != nil { return } var req struct { ID int64 `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params"` } if err := json.Unmarshal(data, &req); err != nil { continue } reply := func(v map[string]any) { b, _ := json.Marshal(map[string]any{"id": req.ID, "result": v}) _ = c.Write(ctx, websocket.MessageText, b) } switch req.Method { case "Page.enable": reply(map[string]any{}) case "Page.navigate": // 事件先于响应到达:验证 Subscribe 必须先行 ev, _ := json.Marshal(map[string]any{"method": "Page.loadEventFired", "params": map[string]any{}}) _ = c.Write(ctx, websocket.MessageText, ev) reply(map[string]any{"frameId": "f1"}) case "Runtime.evaluate": reply(map[string]any{"result": map[string]any{"type": "string", "value": title}}) case "Page.captureScreenshot": reply(map[string]any{"data": tinyPNG}) default: b, _ := json.Marshal(map[string]any{"id": req.ID, "error": map[string]any{"code": -32601, "message": "unknown method"}}) _ = c.Write(ctx, websocket.MessageText, b) } } } } // TestConnCallAndEventTiming:事件先到时 Call 仍能配对响应、Subscribe 先行能等到事件。 func TestConnCallAndEventTiming(t *testing.T) { srv := httptest.NewServer(fakeCDPHandler(t, "Fake Page")) defer srv.Close() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/devtools/page/t1" ctx := context.Background() conn, err := Dial(ctx, wsURL) if err != nil { t.Fatal(err) } defer conn.Close() loaded := conn.Subscribe("Page.loadEventFired") if _, err := conn.Call(ctx, "Page.enable", nil); err != nil { t.Fatal(err) } if _, err := conn.Call(ctx, "Page.navigate", map[string]any{"url": "https://example.com"}); err != nil { t.Fatalf("navigate: %v", err) } // 事件在响应前已发出:订阅先行则必然能等到 if _, err := loaded.Wait(ctx, 2*time.Second); err != nil { t.Fatalf("event wait: %v", err) } title, err := evaluateString(ctx, conn, "document.title") if err != nil || title != "Fake Page" { t.Fatalf("evaluate: %q %v", title, err) } // 服务端错误响应 → Go error if _, err := conn.Call(ctx, "NoSuch.Method", nil); err == nil || !strings.Contains(err.Error(), "unknown method") { t.Fatalf("expected rpc error, got %v", err) } } // TestConnClosed:连接断开后 Call 立即失败。 func TestConnClosed(t *testing.T) { srv := httptest.NewServer(fakeCDPHandler(t, "x")) defer srv.Close() wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/devtools/page/t1" ctx := context.Background() conn, err := Dial(ctx, wsURL) if err != nil { t.Fatal(err) } conn.Close() if _, err := conn.Call(ctx, "Page.enable", nil); err == nil { t.Fatal("call on closed connection should fail") } }