diff --git a/browser/pool.go b/browser/pool.go index fc43566..07bb6cb 100644 --- a/browser/pool.go +++ b/browser/pool.go @@ -97,11 +97,18 @@ func (p *Pool) Render(ctx context.Context, rawURL string) (RenderResult, error) return RenderResult{}, err } - page, err := stealth.Page(b) + // Create the page in the background so headful mode does not pull the Chrome + // window to the foreground and steal focus on every new tab. The only + // difference from stealth.Page is Background:true; the anti-detection script + // is still injected via stealth.JS. + page, err := b.Page(proto.TargetCreateTarget{Background: true}) if err != nil { return RenderResult{}, fmt.Errorf("new page: %w", err) } defer func() { _ = page.Close() }() + if _, err := page.EvalOnNewDocument(stealth.JS); err != nil { + return RenderResult{}, fmt.Errorf("inject stealth: %w", err) + } page = page.Context(ctx).Timeout(p.opts.RenderTimeout) diff --git a/browser/pool_test.go b/browser/pool_test.go index 4c14a78..1fd1b89 100644 --- a/browser/pool_test.go +++ b/browser/pool_test.go @@ -126,6 +126,45 @@ func TestRenderCapturesFinalDOM(t *testing.T) { } } +// TestRenderInjectsStealthScript verifies that Render still injects the stealth +// anti-detection script after switching from stealth.Page(b) to a manual +// b.Page(Background:true) + EvalOnNewDocument(stealth.JS). The stealth script +// normalizes navigator.languages to ["en-US","en"] (without it, Chrome reports +// ["en"]), which serves as a reliable indicator that injection succeeded. +func TestRenderInjectsStealthScript(t *testing.T) { + if testing.Short() { + t.Skip("render test drives Chrome; skipped under -short") + } + if _, ok := LookChrome(); !ok { + t.Skip("no Chrome/Chromium found; skipping render test") + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + // Page JS writes navigator.languages into the DOM for assertion. + _, _ = w.Write([]byte(` +
+ +`)) + })) + defer srv.Close() + + p := New(Options{Headless: true, Workers: 1, Settle: 300 * time.Millisecond, RenderTimeout: 20 * time.Second}) + defer func() { _ = p.Close() }() + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + res, err := p.Render(ctx, srv.URL) + if err != nil { + t.Fatalf("render: %v", err) + } + // With stealth injected, navigator.languages is ["en-US","en"]; without it, ["en"]. + if !strings.Contains(res.HTML, `["en-US","en"]`) { + t.Errorf("stealth script not injected: navigator.languages should be [\"en-US\",\"en\"], got HTML:\n%s", res.HTML) + } +} + func TestIsHTML(t *testing.T) { cases := []struct { ct string