From 6647e4f63dc47223b37b59df60142c990aacb188 Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Tue, 24 Feb 2026 01:28:09 +0000 Subject: [PATCH] fix: set default viewport for NewBrowser and align User-Agent with engine NewBrowser previously had no viewport (strong headless signal) and used a Firefox User-Agent unconditionally, even for Chromium instances (detectable mismatch). Add per-engine UA constants (DefaultFirefoxUserAgent, DefaultChromiumUserAgent) and auto-select the matching UA in initBrowser when the caller hasn't set one explicitly. Keep DefaultUserAgent as a backward-compatible alias. Add 1920x1080 default viewport to NewBrowser (most common desktop resolution). NewInteractiveBrowser keeps its existing 1280x720 default but also gains engine-aware UA selection. Closes #70 Co-Authored-By: Claude Opus 4.6 --- browser_init.go | 10 ++++++++ interactive.go | 7 +++--- playwright.go | 23 +++++++++++------ stealth_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/browser_init.go b/browser_init.go index e5e8c62..5236adf 100644 --- a/browser_init.go +++ b/browser_init.go @@ -52,6 +52,16 @@ func initBrowser(opt BrowserOptions) (*browserInitResult, error) { return nil, ErrInvalidBrowserSelection } + // Auto-select a User-Agent matching the browser engine when the caller hasn't set one. + if opt.UserAgent == "" { + switch opt.Browser { + case BrowserChromium: + opt.UserAgent = DefaultChromiumUserAgent + default: + opt.UserAgent = DefaultFirefoxUserAgent + } + } + // Collect launch args and init scripts, starting with any stealth-mode presets. stealth := opt.Stealth == nil || *opt.Stealth var launchArgs []string diff --git a/interactive.go b/interactive.go index b2e0889..29bc577 100644 --- a/interactive.go +++ b/interactive.go @@ -59,10 +59,9 @@ type interactiveBrowser struct { func NewInteractiveBrowser(ctx context.Context, opts ...BrowserOptions) (InteractiveBrowser, error) { var thirtySeconds = 30 * time.Second opt := mergeOptions(BrowserOptions{ - UserAgent: DefaultUserAgent, - Browser: BrowserFirefox, - Timeout: &thirtySeconds, - Stealth: Bool(true), + Browser: BrowserFirefox, + Timeout: &thirtySeconds, + Stealth: Bool(true), Dimensions: Size{ Width: 1280, Height: 720, diff --git a/playwright.go b/playwright.go index b8e0a81..bacba5b 100644 --- a/playwright.go +++ b/playwright.go @@ -36,8 +36,14 @@ const ( BrowserWebKit BrowserSelection = "webkit" ) -// DefaultUserAgent is the user-agent string used by all browser instances. -const DefaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" +// DefaultFirefoxUserAgent is the user-agent string used for Firefox browser instances. +const DefaultFirefoxUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:147.0) Gecko/20100101 Firefox/147.0" + +// DefaultChromiumUserAgent is the user-agent string used for Chromium browser instances. +const DefaultChromiumUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" + +// DefaultUserAgent is an alias for DefaultFirefoxUserAgent, retained for backward compatibility. +const DefaultUserAgent = DefaultFirefoxUserAgent // Bool returns a pointer to the given bool value. func Bool(v bool) *bool { return &v } @@ -47,7 +53,7 @@ type Size struct { Height int } type BrowserOptions struct { - UserAgent string // If empty, defaults to DefaultUserAgent + UserAgent string // If empty, auto-selected based on Browser engine Browser BrowserSelection // If unset defaults to Firefox. Timeout *time.Duration // If unset defaults to 30 seconds timeout. If set to 0, no timeout @@ -145,10 +151,13 @@ func playwrightCookieToCookie(cookie playwright.Cookie) Cookie { func NewBrowser(ctx context.Context, opts ...BrowserOptions) (Browser, error) { var thirtySeconds = 30 * time.Second opt := mergeOptions(BrowserOptions{ - UserAgent: DefaultUserAgent, - Browser: BrowserFirefox, - Timeout: &thirtySeconds, - Stealth: Bool(true), + Browser: BrowserFirefox, + Timeout: &thirtySeconds, + Stealth: Bool(true), + Dimensions: Size{ + Width: 1920, + Height: 1080, + }, }, opts) if err := ctx.Err(); err != nil { diff --git a/stealth_test.go b/stealth_test.go index 5c8f997..5897079 100644 --- a/stealth_test.go +++ b/stealth_test.go @@ -374,3 +374,68 @@ func TestStealthFirefoxScripts_NoChromiumMarkers(t *testing.T) { } } } + +// --- User-Agent constants --- + +func TestDefaultUserAgent_BackwardCompat(t *testing.T) { + if DefaultUserAgent != DefaultFirefoxUserAgent { + t.Fatal("DefaultUserAgent must equal DefaultFirefoxUserAgent for backward compatibility") + } +} + +func TestDefaultFirefoxUserAgent_Content(t *testing.T) { + if !strings.Contains(DefaultFirefoxUserAgent, "Firefox") { + t.Fatal("DefaultFirefoxUserAgent must contain 'Firefox'") + } + if strings.Contains(DefaultFirefoxUserAgent, "Chrome") { + t.Fatal("DefaultFirefoxUserAgent must not contain 'Chrome'") + } +} + +func TestDefaultChromiumUserAgent_Content(t *testing.T) { + if !strings.Contains(DefaultChromiumUserAgent, "Chrome") { + t.Fatal("DefaultChromiumUserAgent must contain 'Chrome'") + } + if strings.Contains(DefaultChromiumUserAgent, "Firefox") { + t.Fatal("DefaultChromiumUserAgent must not contain 'Firefox'") + } +} + +// --- Viewport and UA defaults via mergeOptions --- + +func TestMergeOptions_DefaultViewport(t *testing.T) { + base := BrowserOptions{ + Dimensions: Size{Width: 1920, Height: 1080}, + } + got := mergeOptions(base, nil) + if got.Dimensions.Width != 1920 || got.Dimensions.Height != 1080 { + t.Fatalf("expected default viewport 1920x1080, got %dx%d", got.Dimensions.Width, got.Dimensions.Height) + } +} + +func TestMergeOptions_ViewportOverride(t *testing.T) { + base := BrowserOptions{ + Dimensions: Size{Width: 1920, Height: 1080}, + } + got := mergeOptions(base, []BrowserOptions{{Dimensions: Size{Width: 1280, Height: 720}}}) + if got.Dimensions.Width != 1280 || got.Dimensions.Height != 720 { + t.Fatalf("expected overridden viewport 1280x720, got %dx%d", got.Dimensions.Width, got.Dimensions.Height) + } +} + +func TestMergeOptions_EmptyUANotOverridden(t *testing.T) { + base := BrowserOptions{} + got := mergeOptions(base, []BrowserOptions{{Browser: BrowserChromium}}) + if got.UserAgent != "" { + t.Fatalf("expected empty UserAgent after merge with no explicit UA, got %q", got.UserAgent) + } +} + +func TestMergeOptions_ExplicitUAPreserved(t *testing.T) { + base := BrowserOptions{} + customUA := "MyCustomAgent/1.0" + got := mergeOptions(base, []BrowserOptions{{UserAgent: customUA}}) + if got.UserAgent != customUA { + t.Fatalf("expected explicit UA %q preserved, got %q", customUA, got.UserAgent) + } +} -- 2.49.1