Add capture browser and step-at-a-time browse tool
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// 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/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")
|
||||
dump := flag.Bool("dump", false, "print page html 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()
|
||||
|
||||
actions := []chromedp.Action{chromedp.EmulateViewport(1280, 800)}
|
||||
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
|
||||
if *dump {
|
||||
actions = append(actions, chromedp.OuterHTML("html", &html, chromedp.ByQuery))
|
||||
} else {
|
||||
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 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)
|
||||
}
|
||||
Reference in New Issue
Block a user