refactor: restructure API, deduplicate code, expand test coverage
Some checks failed
CI / build (push) Failing after 2m4s
CI / test (push) Failing after 2m6s
CI / vet (push) Failing after 2m19s

- 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>
This commit is contained in:
2026-02-09 13:59:47 -05:00
parent e7b7e78796
commit cb2ed10cfd
32 changed files with 667 additions and 417 deletions

View File

@@ -4,9 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"time"
"github.com/playwright-community/playwright-go"
@@ -24,7 +22,7 @@ type playWrightBrowser struct {
var _ Browser = playWrightBrowser{}
type PlayWrightBrowserSelection string
type BrowserSelection string
var (
ErrInvalidBrowserSelection = errors.New("invalid browser selection")
@@ -33,18 +31,18 @@ var (
)
const (
PlayWrightBrowserSelectionChromium PlayWrightBrowserSelection = "chromium"
PlayWrightBrowserSelectionFirefox PlayWrightBrowserSelection = "firefox"
PlayWrightBrowserSelectionWebKit PlayWrightBrowserSelection = "webkit"
BrowserChromium BrowserSelection = "chromium"
BrowserFirefox BrowserSelection = "firefox"
BrowserWebKit BrowserSelection = "webkit"
)
type Size struct {
Width int
Height int
}
type PlayWrightBrowserOptions struct {
type BrowserOptions struct {
UserAgent string // If empty, defaults to "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0"
Browser PlayWrightBrowserSelection // If unset defaults to Firefox.
Browser BrowserSelection // If unset defaults to Firefox.
Timeout *time.Duration // If unset defaults to 30 seconds timeout. If set to 0, no timeout
// CookieJar will, if set, load all cookies from the cookie jar into the browser and save all cookies from the
@@ -56,15 +54,15 @@ type PlayWrightBrowserOptions struct {
Dimensions Size
DarkMode bool
// PlayWrightServerAddress is the address of a PlayWright server to connect to.
// ServerAddress is the address of a Playwright server to connect to.
// Defaults to the value of the environment variable PLAYWRIGHT_SERVER_ADDRESS.
PlayWrightServerAddress string
ServerAddress string
// DontLaunchOnConnectFailure will, if set, not launch the browser if the connection to the PlayWright server,
// and return an error if the connection fails.
DontLaunchOnConnectFailure bool
// RequireServer will, if set, return an error if the connection to the
// Playwright server fails instead of falling back to a local browser launch.
RequireServer bool
// UseLocalOnly will, if set, not connect to the PlayWright server, and instead use the local PlayWright server.
// UseLocalOnly will, if set, not connect to the Playwright server, and instead launch a local browser.
UseLocalOnly bool
}
@@ -90,48 +88,14 @@ func playwrightCookieToCookie(cookie playwright.Cookie) Cookie {
}
}
func NewPlayWrightBrowser(ctx context.Context, opts ...PlayWrightBrowserOptions) (Browser, error) {
func NewBrowser(ctx context.Context, opts ...BrowserOptions) (Browser, error) {
var thirtySeconds = 30 * time.Second
opt := PlayWrightBrowserOptions{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0",
Browser: PlayWrightBrowserSelectionFirefox,
Timeout: &thirtySeconds,
DarkMode: false,
PlayWrightServerAddress: "",
}
opt := mergeOptions(BrowserOptions{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:142.0) Gecko/20100101 Firefox/142.0",
Browser: BrowserFirefox,
Timeout: &thirtySeconds,
}, opts)
for _, o := range opts {
if o.UserAgent != "" {
opt.UserAgent = o.UserAgent
}
if o.Browser != "" {
opt.Browser = o.Browser
}
if o.Timeout != nil {
opt.Timeout = o.Timeout
}
if o.CookieJar != nil {
opt.CookieJar = o.CookieJar
}
if o.Dimensions.Width > 0 && o.Dimensions.Height > 0 {
opt.Dimensions = o.Dimensions
}
if o.DarkMode {
opt.DarkMode = true
}
if o.PlayWrightServerAddress != "" {
opt.PlayWrightServerAddress = o.PlayWrightServerAddress
}
if o.DontLaunchOnConnectFailure {
opt.DontLaunchOnConnectFailure = true
}
if o.UseLocalOnly {
opt.UseLocalOnly = true
}
opt.ShowBrowser = o.ShowBrowser
}
// Check if context is already done
if err := ctx.Err(); err != nil {
return nil, err
}
@@ -141,145 +105,28 @@ func NewPlayWrightBrowser(ctx context.Context, opts ...PlayWrightBrowserOptions)
err error
}
// Create a channel for the result
resultCh := make(chan browserResult, 1)
// Launch browser initialization in a separate goroutine
go func() {
pw, err := playwright.Run()
if err != nil {
err = playwright.Install()
if err != nil {
resultCh <- browserResult{nil, err}
return
}
pw, err = playwright.Run()
if err != nil {
resultCh <- browserResult{nil, err}
return
}
}
var bt playwright.BrowserType
switch opt.Browser {
case PlayWrightBrowserSelectionChromium:
bt = pw.Chromium
if opt.PlayWrightServerAddress == "" {
opt.PlayWrightServerAddress = os.Getenv("PLAYWRIGHT_SERVER_ADDRESS_CHROMIUM")
}
case PlayWrightBrowserSelectionFirefox:
bt = pw.Firefox
if opt.PlayWrightServerAddress == "" {
opt.PlayWrightServerAddress = os.Getenv("PLAYWRIGHT_SERVER_ADDRESS_FIREFOX")
}
case PlayWrightBrowserSelectionWebKit:
bt = pw.WebKit
if opt.PlayWrightServerAddress == "" {
opt.PlayWrightServerAddress = os.Getenv("PLAYWRIGHT_SERVER_ADDRESS_WEBKIT")
}
default:
resultCh <- browserResult{nil, ErrInvalidBrowserSelection}
return
}
var browser playwright.Browser
var launch = true
if opt.PlayWrightServerAddress != "" && !opt.UseLocalOnly {
launch = false
slog.Info("connecting to playwright server", "address", opt.PlayWrightServerAddress)
var timeout float64 = 30000
browser, err = bt.Connect(opt.PlayWrightServerAddress, playwright.BrowserTypeConnectOptions{Timeout: &timeout})
if err != nil {
if opt.DontLaunchOnConnectFailure {
resultCh <- browserResult{nil, err}
return
}
slog.Warn("failed to connect to playwright server, launching local browser", "err", err)
launch = true
}
}
if launch {
browser, err = bt.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(!opt.ShowBrowser),
})
if err != nil {
resultCh <- browserResult{nil, err}
return
}
}
var viewport *playwright.Size
if opt.Dimensions.Width > 0 && opt.Dimensions.Height > 0 {
viewport = &playwright.Size{
Width: opt.Dimensions.Width,
Height: opt.Dimensions.Height,
}
}
var scheme *playwright.ColorScheme
if opt.DarkMode {
scheme = playwright.ColorSchemeDark
} else {
scheme = playwright.ColorSchemeNoPreference
}
c, err := browser.NewContext(playwright.BrowserNewContextOptions{
UserAgent: playwright.String(opt.UserAgent),
Viewport: viewport,
ColorScheme: scheme,
})
res, err := initBrowser(opt)
if err != nil {
resultCh <- browserResult{nil, err}
return
}
if opt.CookieJar != nil {
cookies, err := opt.CookieJar.GetAll()
if err != nil {
resultCh <- browserResult{nil, fmt.Errorf("error getting cookies from cookie jar: %w", err)}
return
}
pwCookies := make([]playwright.OptionalCookie, len(cookies))
for i, cookie := range cookies {
pwCookies[i] = cookieToPlaywrightOptionalCookie(cookie)
}
err = c.AddCookies(pwCookies)
if err != nil {
resultCh <- browserResult{nil, fmt.Errorf("error adding cookies to browser: %w", err)}
return
}
}
resultCh <- browserResult{
browser: playWrightBrowser{
pw: pw,
browser: browser,
userAgent: opt.UserAgent,
timeout: *opt.Timeout,
cookieJar: opt.CookieJar,
ctx: c,
serverAddr: opt.PlayWrightServerAddress,
pw: res.pw,
browser: res.browser,
userAgent: res.opt.UserAgent,
timeout: *res.opt.Timeout,
cookieJar: res.opt.CookieJar,
ctx: res.bctx,
serverAddr: res.opt.ServerAddress,
},
err: nil,
}
}()
// Wait for either context cancellation or browser initialization completion
select {
case <-ctx.Done():
return nil, ctx.Err()
@@ -367,12 +214,9 @@ func (b playWrightBrowser) Close() error {
)
}
func deferClose(cl io.Closer) {
_ = cl.Close()
}
func Screenshot(ctx context.Context, target string, timeout time.Duration) ([]byte, error) {
browser, err := NewPlayWrightBrowser(ctx, PlayWrightBrowserOptions{
browser, err := NewBrowser(ctx, BrowserOptions{
Timeout: &timeout,
})
@@ -380,14 +224,14 @@ func Screenshot(ctx context.Context, target string, timeout time.Duration) ([]by
return nil, fmt.Errorf("error creating browser: %w", err)
}
defer deferClose(browser)
defer DeferClose(browser)
doc, err := browser.Open(ctx, target, OpenPageOptions{})
if err != nil {
return nil, fmt.Errorf("error opening page: %w", err)
}
defer deferClose(doc)
defer DeferClose(doc)
return doc.Screenshot()
}