// Command browse drives the capture browser one step at a time: act, capture, report. package main import ( "context" "encoding/json" "flag" "fmt" "log" "net/http" "os" "path/filepath" "strconv" "strings" "time" "github.com/chromedp/cdproto/input" "github.com/chromedp/cdproto/network" "github.com/chromedp/cdproto/target" "github.com/chromedp/chromedp" ) type pageTarget struct { ID string `json:"id"` Type string `json:"type"` URL string `json:"url"` Title string `json:"title"` } func currentTarget() (string, error) { resp, err := http.Get("http://localhost:9222/json/list") if err != nil { return "", fmt.Errorf("capture browser not reachable on localhost:9222, run tools/capturebrowser first: %w", err) } defer resp.Body.Close() targets := []pageTarget{} if err := json.NewDecoder(resp.Body).Decode(&targets); err != nil { return "", err } for _, t := range targets { if t.Type != "page" { continue } if strings.HasPrefix(t.URL, "devtools://") || strings.HasPrefix(t.URL, "chrome-extension://") { continue } return t.ID, nil } return newTab() } func newTab() (string, error) { req, err := http.NewRequest(http.MethodPut, "http://localhost:9222/json/new?url=about:blank", nil) if err != nil { return "", err } resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() t := pageTarget{} if err := json.NewDecoder(resp.Body).Decode(&t); err != nil { return "", err } if t.ID == "" { return "", fmt.Errorf("capture browser did not create a tab") } return t.ID, nil } func parseXY(coords string) (float64, float64, error) { parts := strings.Split(coords, ",") if len(parts) != 2 { return 0, 0, fmt.Errorf("click coordinates must be x,y, got %q", coords) } x, err := strconv.ParseFloat(strings.TrimSpace(parts[0]), 64) if err != nil { return 0, 0, err } y, err := strconv.ParseFloat(strings.TrimSpace(parts[1]), 64) if err != nil { return 0, 0, err } return x, y, nil } func keyChord(name string) string { switch strings.ToLower(name) { case "enter": return "\r" case "tab": return "\t" case "escape": return "" case "backspace": return "\b" default: return name } } func main() { nav := flag.String("nav", "", "navigate to url") back := flag.Bool("back", false, "navigate back in history") scroll := flag.Int("scroll", 0, "scroll vertically by pixels, negative scrolls up") clickSel := flag.String("clicksel", "", "click the first element matching css selector") click := flag.String("click", "", "click at viewport coordinates x,y as shown in the screenshot") typeText := flag.String("type", "", "insert text into the focused element") key := flag.String("key", "", "press a key: enter, tab, escape, backspace, or a literal character") wait := flag.String("wait", "", "css selector that must be visible before capturing") cookie := flag.String("cookie", "", "set a name=value cookie on localhost before acting") mobile := flag.Bool("mobile", false, "emulate a phone viewport (390x844, touch) instead of desktop 1280x800") size := flag.String("size", "", "viewport size as WxH, overriding the desktop default") dump := flag.Bool("dump", false, "print page html instead of writing a screenshot") eval := flag.String("eval", "", "evaluate javascript in the page and print the json result instead of writing a screenshot") out := flag.String("out", "screenshots/browse.png", "output png path") flag.Parse() id, err := currentTarget() if err != nil { log.Fatalf("[ERROR] %v", err) } allocCtx, _ := chromedp.NewRemoteAllocator(context.Background(), "http://localhost:9222") // cancelling the chromedp context closes the attached tab; the tab must outlive this process ctx, _ := chromedp.NewContext(allocCtx, chromedp.WithTargetID(target.ID(id))) ctx, cancelTimeout := context.WithTimeout(ctx, 15*time.Second) defer cancelTimeout() viewport := chromedp.EmulateViewport(1280, 800) if *mobile { viewport = chromedp.EmulateViewport(390, 844, chromedp.EmulateMobile) } if *size != "" { w, h, ok := strings.Cut(*size, "x") width, werr := strconv.ParseInt(w, 10, 64) height, herr := strconv.ParseInt(h, 10, 64) if !ok || werr != nil || herr != nil { log.Fatalf("[ERROR] size must be WxH, got %q", *size) } viewport = chromedp.EmulateViewport(width, height) } actions := []chromedp.Action{viewport} if *cookie != "" { name, value, ok := strings.Cut(*cookie, "=") if !ok { log.Fatalf("[ERROR] cookie must be name=value, got %q", *cookie) } actions = append(actions, network.SetCookie(name, value).WithDomain("localhost").WithPath("/")) } if *nav != "" { actions = append(actions, chromedp.Navigate(*nav)) } if *back { actions = append(actions, chromedp.NavigateBack()) } if *scroll != 0 { actions = append(actions, chromedp.Evaluate(fmt.Sprintf("window.scrollBy(0, %d)", *scroll), nil)) } if *clickSel != "" { actions = append(actions, chromedp.Click(*clickSel, chromedp.ByQuery)) } if *click != "" { x, y, err := parseXY(*click) if err != nil { log.Fatalf("[ERROR] %v", err) } actions = append(actions, chromedp.MouseClickXY(x, y)) } if *typeText != "" { actions = append(actions, input.InsertText(*typeText)) } if *key != "" { actions = append(actions, chromedp.KeyEvent(keyChord(*key))) } if *wait != "" { actions = append(actions, chromedp.WaitVisible(*wait, chromedp.ByQuery)) } actions = append(actions, chromedp.Sleep(700*time.Millisecond)) var html string var png []byte var evalResult any switch { case *dump: actions = append(actions, chromedp.OuterHTML("html", &html, chromedp.ByQuery)) case *eval != "": actions = append(actions, chromedp.Evaluate(*eval, &evalResult)) default: actions = append(actions, chromedp.CaptureScreenshot(&png)) } var location, title string actions = append(actions, chromedp.Location(&location), chromedp.Title(&title)) if err := chromedp.Run(ctx, actions...); err != nil { log.Fatalf("[ERROR] browse: %v", err) } if *dump { fmt.Println(html) } else if *eval != "" { encoded, err := json.MarshalIndent(evalResult, "", " ") if err != nil { log.Fatalf("[ERROR] encode eval result: %v", err) } fmt.Println(string(encoded)) } else { if err := os.MkdirAll(filepath.Dir(*out), 0o755); err != nil { log.Fatalf("[ERROR] create output dir: %v", err) } if err := os.WriteFile(*out, png, 0o644); err != nil { log.Fatalf("[ERROR] write %s: %v", *out, err) } } fmt.Printf("url: %s\ntitle: %s\n", location, title) }