| <unsafe> & "quoted" |
fragment only
`) case "/api": w.Header().Set("Content-Type", "application/json") fmt.Fprint(w, `{"kind":"api","ok":true}`) default: http.NotFound(w, request) } } func runRealBrowser(t *testing.T, chrome, target, want string, afterStart func()) string { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), realBrowserTimeout) defer cancel() toBrowserRead, toBrowserWrite, err := os.Pipe() if err != nil { t.Fatal(err) } fromBrowserRead, fromBrowserWrite, err := os.Pipe() if err != nil { _ = toBrowserRead.Close() _ = toBrowserWrite.Close() t.Fatal(err) } defer toBrowserWrite.Close() defer fromBrowserRead.Close() stderrPath := filepath.Join(t.TempDir(), "chrome.stderr") stderr, err := os.OpenFile(stderrPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err != nil { t.Fatal(err) } devNull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0) if err != nil { _ = stderr.Close() t.Fatal(err) } command := exec.CommandContext(ctx, chrome, "--headless=new", "--disable-background-networking", "--disable-component-update", "--disable-default-apps", "--disable-sync", "--metrics-recording-only", "--no-first-run", "--no-default-browser-check", "--user-data-dir="+t.TempDir(), "--remote-debugging-pipe", "about:blank", ) command.ExtraFiles = []*os.File{toBrowserRead, fromBrowserWrite} command.Stdout = devNull command.Stderr = stderr command.WaitDelay = 2 * time.Second if err := command.Start(); err != nil { _ = toBrowserRead.Close() _ = fromBrowserWrite.Close() _ = stderr.Close() _ = devNull.Close() t.Fatalf("start real browser: %v", err) } _ = toBrowserRead.Close() _ = fromBrowserWrite.Close() client := &devToolsPipe{reader: bufio.NewReader(fromBrowserRead), writer: toBrowserWrite} targetResult := client.call(t, "", "Target.createTarget", map[string]any{"url": target}) var created struct { TargetID string `json:"targetId"` } if err := json.Unmarshal(targetResult, &created); err != nil || created.TargetID == "" { t.Fatalf("decode Chrome target: %v", err) } attachResult := client.call(t, "", "Target.attachToTarget", map[string]any{"targetId": created.TargetID, "flatten": true}) var attached struct { SessionID string `json:"sessionId"` } if err := json.Unmarshal(attachResult, &attached); err != nil || attached.SessionID == "" { t.Fatalf("decode Chrome session: %v", err) } client.call(t, attached.SessionID, "Runtime.enable", map[string]any{}) waitForBrowserDOM(t, client, attached.SessionID, "") if afterStart != nil { afterStart() } document := waitForBrowserDOM(t, client, attached.SessionID, want) _ = client.callIgnoringClose("", "Browser.close", map[string]any{}) waitResult := make(chan error, 1) go func() { waitResult <- command.Wait() }() select { case err := <-waitResult: if err != nil && !errors.Is(err, exec.ErrWaitDelay) { _ = stderr.Close() contents, _ := os.ReadFile(stderrPath) t.Fatalf("real browser failed: %v; stderr=%s", err, boundedBrowserOutput(string(contents))) } case <-time.After(5 * time.Second): _ = command.Process.Kill() t.Fatal("real browser did not exit after Browser.close") } _ = stderr.Close() _ = devNull.Close() return document } type devToolsPipe struct { reader *bufio.Reader writer io.Writer nextID int } type devToolsEnvelope struct { ID int `json:"id"` Result json.RawMessage `json:"result"` Error *struct { Code int `json:"code"` Message string `json:"message"` } `json:"error"` } func (c *devToolsPipe) call(t *testing.T, session, method string, parameters map[string]any) json.RawMessage { t.Helper() c.nextID++ request := map[string]any{"id": c.nextID, "method": method, "params": parameters} if session != "" { request["sessionId"] = session } contents, err := json.Marshal(request) if err != nil { t.Fatal(err) } contents = append(contents, 0) if _, err := c.writer.Write(contents); err != nil { t.Fatalf("write Chrome DevTools request: %v", err) } for { message, err := c.reader.ReadBytes(0) if err != nil { t.Fatalf("read Chrome DevTools response: %v", err) } if len(message) > 4<<20 { t.Fatal("Chrome DevTools response exceeds 4 MiB") } var response devToolsEnvelope if err := json.Unmarshal(bytes.TrimSuffix(message, []byte{0}), &response); err != nil { t.Fatalf("decode Chrome DevTools response: %v", err) } if response.ID != c.nextID { continue } if response.Error != nil { t.Fatalf("Chrome DevTools %s failed (%d): %s", method, response.Error.Code, response.Error.Message) } return response.Result } } func (c *devToolsPipe) callIgnoringClose(session, method string, parameters map[string]any) error { c.nextID++ request := map[string]any{"id": c.nextID, "method": method, "params": parameters} if session != "" { request["sessionId"] = session } contents, err := json.Marshal(request) if err != nil { return err } _, err = c.writer.Write(append(contents, 0)) return err } func waitForBrowserDOM(t *testing.T, client *devToolsPipe, session, want string) string { t.Helper() deadline := time.Now().Add(10 * time.Second) var last string for time.Now().Before(deadline) { result := client.call(t, session, "Runtime.evaluate", map[string]any{ "expression": "document.documentElement && document.documentElement.outerHTML", "returnByValue": true, }) var evaluated struct { Result struct { Type string `json:"type"` Value string `json:"value"` } `json:"result"` } if err := json.Unmarshal(result, &evaluated); err == nil && evaluated.Result.Type == "string" { last = evaluated.Result.Value if last != "" && (want == "" || strings.Contains(last, want)) { return last } } time.Sleep(20 * time.Millisecond) } t.Fatalf("browser DOM did not contain %q; last DOM:\n%s", want, last) return "" } func waitForRealBrowserSubscriber(t *testing.T, hub *eventHub) { t.Helper() deadline := time.Now().Add(10 * time.Second) for time.Now().Before(deadline) { hub.mu.Lock() count := len(hub.subscribers) hub.mu.Unlock() if count != 0 { return } time.Sleep(10 * time.Millisecond) } t.Fatal("real browser did not establish the same-origin event stream") } func boundedBrowserOutput(value string) string { const limit = 4096 if len(value) <= limit { return value } return value[:limit] + "\n[Chrome output truncated]" }