fix(browser): make Lightpanda backend usable end-to-end

Live testing surfaced three issues:

- Lightpanda numbers targets per-browser, and each conn is its own
  browser, so every tab gets the same upstream targetID
  ("FID-0000000001"). Synthesize globally-unique "lp-N" keys for our
  internal map so multi-tenant tab tracking works.
- page.Info() returns valid data once post-open then errors on
  subsequent calls, which made ListTabs silently drop tabs. Cache URL
  and Title at OpenTab time and read from the cache in ListTabs.
- rod.Browser.Close() calls Browser.close which Lightpanda doesn't
  implement; the WS drops cleanly anyway. Swallow the error to quiet
  the noisy log line.

Two Lightpanda upstream bugs are documented in docs/browser-backends.md
and exercised by TestLightpanda_KnownUpstreamGaps:
  1. Accessibility.getFullAXTree returns nodeId as a JSON number
     (CDP spec: string)
  2. Runtime.evaluate rejects go-rod's function-apply wrapper
This commit is contained in:
Pierre Tachoire committed 2026-07-01 10:23:20 +02:00
1 parent 890ced810d
commit 4147fee4e5
4 files changed
+81 -21

No files matched your search

+38 -6
View File
@@ -64,13 +64,11 @@ func TestLightpanda_SingleTenant_Golden(t *testing.T) {
if tab.TargetID == "" {
t.Fatal("expected non-empty TargetID")
}
snap, err := m.Snapshot(ctx, tab.TargetID, browser.DefaultSnapshotOptions())
if err != nil {
t.Fatalf("snapshot: %v", err)
if tab.URL != "https://example.com" {
t.Errorf("expected URL https://example.com, got %q", tab.URL)
}
if snap.Snapshot == "" {
t.Fatal("expected non-empty AX snapshot")
if tab.Title == "" {
t.Errorf("expected non-empty Title (page.Info() should populate from initial post-open call)")
}
tabs, err := m.ListTabs(ctx)
@@ -80,6 +78,9 @@ func TestLightpanda_SingleTenant_Golden(t *testing.T) {
if len(tabs) != 1 {
t.Fatalf("expected 1 tab, got %d", len(tabs))
}
if tabs[0].Title != tab.Title {
t.Errorf("ListTabs should return cached title %q, got %q", tab.Title, tabs[0].Title)
}
if err := m.CloseTab(ctx, tab.TargetID); err != nil {
t.Fatalf("close tab: %v", err)
@@ -90,6 +91,37 @@ func TestLightpanda_SingleTenant_Golden(t *testing.T) {
}
}
// TestLightpanda_KnownUpstreamGaps documents Lightpanda CDP-conformance bugs
// that goclaw cannot work around without a custom decoder. Filed for awareness;
// these need fixing in Lightpanda upstream.
func TestLightpanda_KnownUpstreamGaps(t *testing.T) {
m := newLightpandaManager(t)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
tab, err := m.OpenTab(ctx, "https://example.com")
if err != nil {
t.Fatalf("open tab: %v", err)
}
// Bug 1: Accessibility.getFullAXTree returns nodeId as JSON number.
// CDP spec says AXNodeId is a string. go-rod's typed proto fails to decode.
if _, err := m.Snapshot(ctx, tab.TargetID, browser.DefaultSnapshotOptions()); err == nil {
t.Log("Lightpanda has fixed AX tree decoding — remove this gap test")
} else if !strings.Contains(err.Error(), "AccessibilityAXNodeID") {
t.Logf("AX tree failed differently than expected (Lightpanda may have updated): %v", err)
}
// Bug 2: Runtime.evaluate via go-rod's function-wrapper fails.
// go-rod wraps JS as a callable; Lightpanda's runtime rejects it.
if _, err := m.Evaluate(ctx, tab.TargetID, "document.title"); err == nil {
t.Log("Lightpanda Evaluate works — remove this gap test")
} else if !strings.Contains(err.Error(), "is not a function") {
t.Logf("Evaluate failed differently than expected: %v", err)
}
}
func TestLightpanda_MultiTenant_Isolation(t *testing.T) {
m := newLightpandaManager(t)