From 96b084c27397a919aab86f04296b70605ab5ad5e Mon Sep 17 00:00:00 2001 From: wangpi26 Date: Thu, 9 Jul 2026 11:27:37 +0800 Subject: [PATCH 1/2] fix: create render pages in background to avoid focus stealing In headful mode (e.g. --control-url attaching to a logged-in browser), every new page created via stealth.Page() was opened in the foreground, which activated the Chrome window and stole focus from the user's active app on each page render. Replace stealth.Page(b) with an explicit b.Page() using TargetCreateTarget{Background: true}, then inject the same stealth.JS anti-detection script via EvalOnNewDocument. Behaviour is identical except pages now open in the background, so focus is no longer disrupted during a crawl. --- browser/pool.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/browser/pool.go b/browser/pool.go index fc43566..cbff549 100644 --- a/browser/pool.go +++ b/browser/pool.go @@ -97,11 +97,16 @@ func (p *Pool) Render(ctx context.Context, rawURL string) (RenderResult, error) return RenderResult{}, err } - page, err := stealth.Page(b) + // 在后台创建页面,避免 headful 模式下每次新建 tab 都把 Chrome 窗口拉到前台抢夺焦点。 + // 与 stealth.Page 的唯一区别是设置了 Background:true;反检测脚本仍由 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) From f6f7dfa32720f6535bc6ed255ff98c7a1a2153d5 Mon Sep 17 00:00:00 2001 From: wangpi26 Date: Tue, 14 Jul 2026 14:47:33 +0800 Subject: [PATCH 2/2] feat: english comments + stealth injection test Per PR review feedback: - Rewrite comments in English - Add TestRenderInjectsStealthScript to verify stealth.JS still injected --- browser/pool.go | 6 ++++-- browser/pool_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/browser/pool.go b/browser/pool.go index cbff549..07bb6cb 100644 --- a/browser/pool.go +++ b/browser/pool.go @@ -97,8 +97,10 @@ func (p *Pool) Render(ctx context.Context, rawURL string) (RenderResult, error) return RenderResult{}, err } - // 在后台创建页面,避免 headful 模式下每次新建 tab 都把 Chrome 窗口拉到前台抢夺焦点。 - // 与 stealth.Page 的唯一区别是设置了 Background:true;反检测脚本仍由 stealth.JS 注入。 + // 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) 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