Files
go-extractor/interactive.go
Steve Dudenhoeffer cb2ed10cfd
Some checks failed
CI / build (push) Failing after 2m4s
CI / test (push) Failing after 2m6s
CI / vet (push) Failing after 2m19s
refactor: restructure API, deduplicate code, expand test coverage
- Extract shared DeferClose helper, removing 14 duplicate copies
- Rename PlayWright-prefixed types to cleaner names (BrowserOptions,
  BrowserSelection, NewBrowser, etc.)
- Rename fields: ServerAddress, RequireServer (was DontLaunchOnConnectFailure)
- Extract shared initBrowser/mergeOptions into browser_init.go,
  deduplicating ~120 lines between NewBrowser and NewInteractiveBrowser
- Remove unused locator field from document struct
- Add tests for all previously untested packages (archive, aislegopher,
  wegmans, useragents, powerball) and expand existing test suites
- Add MIGRATION.md documenting all breaking API changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 13:59:47 -05:00

223 lines
5.8 KiB
Go

package extractor
import (
"context"
"fmt"
"time"
"github.com/playwright-community/playwright-go"
)
// InteractiveBrowser provides low-level page control for interactive browser sessions.
// Unlike Browser which is designed for scraping, InteractiveBrowser exposes mouse, keyboard,
// screenshot, and navigation APIs suitable for remote browser control.
type InteractiveBrowser interface {
// Navigate goes to the given URL and returns the final URL after any redirects.
Navigate(url string) (string, error)
// GoBack navigates back in history. Returns the final URL.
GoBack() (string, error)
// GoForward navigates forward in history. Returns the final URL.
GoForward() (string, error)
// URL returns the current page URL.
URL() string
// MouseClick clicks at the given coordinates with the specified button ("left", "middle", "right").
MouseClick(x, y float64, button string) error
// MouseMove moves the mouse to the given coordinates.
MouseMove(x, y float64) error
// MouseWheel scrolls by the given delta.
MouseWheel(deltaX, deltaY float64) error
// KeyboardType types the given text as if it were entered character by character.
KeyboardType(text string) error
// KeyboardPress presses a special key (e.g. "Enter", "Tab", "Backspace").
KeyboardPress(key string) error
// KeyboardInsertText inserts text directly into the focused element by dispatching
// only an input event (no keydown, keypress, or keyup). This is more reliable than
// KeyboardType for pasting into password fields and custom input components.
KeyboardInsertText(text string) error
// Screenshot takes a full-page screenshot as JPEG with the given quality (0-100).
Screenshot(quality int) ([]byte, error)
// Cookies returns all cookies from the browser context.
Cookies() ([]Cookie, error)
// Close tears down the browser.
Close() error
}
type interactiveBrowser struct {
pw *playwright.Playwright
browser playwright.Browser
ctx playwright.BrowserContext
page playwright.Page
}
// NewInteractiveBrowser creates a headless browser with a page ready for interactive control.
// The context is only used for cancellation during setup.
func NewInteractiveBrowser(ctx context.Context, opts ...BrowserOptions) (InteractiveBrowser, error) {
var thirtySeconds = 30 * time.Second
opt := mergeOptions(BrowserOptions{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0",
Browser: BrowserChromium,
Timeout: &thirtySeconds,
Dimensions: Size{
Width: 1280,
Height: 720,
},
}, opts)
if err := ctx.Err(); err != nil {
return nil, err
}
type result struct {
ib InteractiveBrowser
err error
}
ch := make(chan result, 1)
go func() {
res, err := initBrowser(opt)
if err != nil {
ch <- result{nil, err}
return
}
page, err := res.bctx.NewPage()
if err != nil {
ch <- result{nil, fmt.Errorf("failed to create page: %w", err)}
return
}
ch <- result{
ib: &interactiveBrowser{
pw: res.pw,
browser: res.browser,
ctx: res.bctx,
page: page,
},
}
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case r := <-ch:
return r.ib, r.err
}
}
func (ib *interactiveBrowser) Navigate(url string) (string, error) {
_, err := ib.page.Goto(url, playwright.PageGotoOptions{
WaitUntil: playwright.WaitUntilStateLoad,
})
if err != nil {
return "", fmt.Errorf("navigation failed: %w", err)
}
return ib.page.URL(), nil
}
func (ib *interactiveBrowser) GoBack() (string, error) {
_, err := ib.page.GoBack()
if err != nil {
return ib.page.URL(), fmt.Errorf("go back failed: %w", err)
}
return ib.page.URL(), nil
}
func (ib *interactiveBrowser) GoForward() (string, error) {
_, err := ib.page.GoForward()
if err != nil {
return ib.page.URL(), fmt.Errorf("go forward failed: %w", err)
}
return ib.page.URL(), nil
}
func (ib *interactiveBrowser) URL() string {
return ib.page.URL()
}
func (ib *interactiveBrowser) MouseClick(x, y float64, button string) error {
var btn *playwright.MouseButton
switch button {
case "right":
btn = playwright.MouseButtonRight
case "middle":
btn = playwright.MouseButtonMiddle
default:
btn = playwright.MouseButtonLeft
}
return ib.page.Mouse().Click(x, y, playwright.MouseClickOptions{Button: btn})
}
func (ib *interactiveBrowser) MouseMove(x, y float64) error {
return ib.page.Mouse().Move(x, y)
}
func (ib *interactiveBrowser) MouseWheel(deltaX, deltaY float64) error {
return ib.page.Mouse().Wheel(deltaX, deltaY)
}
func (ib *interactiveBrowser) KeyboardType(text string) error {
return ib.page.Keyboard().Type(text)
}
func (ib *interactiveBrowser) KeyboardPress(key string) error {
return ib.page.Keyboard().Press(key)
}
func (ib *interactiveBrowser) KeyboardInsertText(text string) error {
return ib.page.Keyboard().InsertText(text)
}
func (ib *interactiveBrowser) Screenshot(quality int) ([]byte, error) {
return ib.page.Screenshot(playwright.PageScreenshotOptions{
Type: playwright.ScreenshotTypeJpeg,
Quality: playwright.Int(quality),
})
}
func (ib *interactiveBrowser) Cookies() ([]Cookie, error) {
pwCookies, err := ib.ctx.Cookies()
if err != nil {
return nil, fmt.Errorf("failed to get cookies: %w", err)
}
cookies := make([]Cookie, len(pwCookies))
for i, c := range pwCookies {
cookies[i] = playwrightCookieToCookie(c)
}
return cookies, nil
}
func (ib *interactiveBrowser) Close() error {
var errs []error
if ib.page != nil {
if err := ib.page.Close(); err != nil {
errs = append(errs, err)
}
}
if ib.ctx != nil {
if err := ib.ctx.Close(); err != nil {
errs = append(errs, err)
}
}
if ib.browser != nil {
if err := ib.browser.Close(); err != nil {
errs = append(errs, err)
}
}
if ib.pw != nil {
if err := ib.pw.Stop(); err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return fmt.Errorf("errors during close: %v", errs)
}
return nil
}