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>
This commit is contained in:
214
playwright.go
214
playwright.go
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user