Main Heading
+Main paragraph
+diff --git a/packages/core/lib/v3/tests/page-snapshot.spec.ts b/packages/core/lib/v3/tests/page-snapshot.spec.ts new file mode 100644 index 000000000..414343cd8 --- /dev/null +++ b/packages/core/lib/v3/tests/page-snapshot.spec.ts @@ -0,0 +1,138 @@ +import { test, expect } from "@playwright/test"; +import { V3 } from "../v3"; +import { v3TestConfig } from "./v3.config"; + +test.describe("Page.snapshot", () => { + let v3: V3; + + test.beforeEach(async () => { + v3 = new V3(v3TestConfig); + await v3.init(); + }); + + test.afterEach(async () => { + await v3?.close?.().catch(() => {}); + }); + + test("returns a hybrid snapshot with combined tree and maps", async () => { + const page = v3.context.pages()[0]; + + const html = ` + + +
Main paragraph
+Simple page
+ + + `; + + await page.goto("data:text/html," + encodeURIComponent(html)); + + const snapshot = await page.snapshot(); + + // perFrame should be present and contain at least one frame entry + expect(snapshot.perFrame).toBeDefined(); + expect(Array.isArray(snapshot.perFrame)).toBe(true); + expect(snapshot.perFrame!.length).toBeGreaterThanOrEqual(1); + + const mainFrame = snapshot.perFrame![0]; + expect(mainFrame.frameId).toBeDefined(); + expect(typeof mainFrame.outline).toBe("string"); + expect(typeof mainFrame.xpathMap).toBe("object"); + expect(typeof mainFrame.urlMap).toBe("object"); + }); +}); diff --git a/packages/core/lib/v3/types/public/page.ts b/packages/core/lib/v3/types/public/page.ts index f141f3d67..3273740c7 100644 --- a/packages/core/lib/v3/types/public/page.ts +++ b/packages/core/lib/v3/types/public/page.ts @@ -11,3 +11,10 @@ export type { ConsoleListener } from "../../understudy/consoleMessage"; export type LoadState = "load" | "domcontentloaded" | "networkidle"; export { Response } from "../../understudy/response"; + +// Snapshot types for page.snapshot() method +export type { + HybridSnapshot, + SnapshotOptions, + PerFrameSnapshot, +} from "../private/snapshot"; diff --git a/packages/core/lib/v3/understudy/page.ts b/packages/core/lib/v3/understudy/page.ts index 6943583b4..081322739 100644 --- a/packages/core/lib/v3/understudy/page.ts +++ b/packages/core/lib/v3/understudy/page.ts @@ -7,7 +7,8 @@ import { CdpConnection } from "./cdp"; import { Frame } from "./frame"; import { FrameLocator } from "./frameLocator"; import { deepLocatorFromPage } from "./deepLocator"; -import { resolveXpathForLocation } from "./a11y/snapshot"; +import { resolveXpathForLocation, captureHybridSnapshot } from "./a11y/snapshot"; +import type { HybridSnapshot, SnapshotOptions } from "../types/private"; import { FrameRegistry } from "./frameRegistry"; import { executionContexts } from "./executionContextRegistry"; import { LoadState } from "../types/public/page"; @@ -2066,6 +2067,26 @@ export class Page { .map((c) => c.substring(0, c.length - 1)); } + // ---- Snapshot API ---- + + /** + * Capture a hybrid DOM + Accessibility snapshot of the page. + * + * Returns a combined tree representation with XPath and URL maps for every + * element. This is the same snapshot format used internally by act/extract/observe. + * + * @param options Optional configuration for the snapshot capture. + * @param options.focusSelector Filter the snapshot to a specific element/subtree. + * Supports XPath (prefixed with `xpath=` or starting with `/`) and CSS with iframe hops via `>>`. + * @param options.pierceShadow Pierce shadow DOM when walking the document (default: true). + * @param options.experimental Enable experimental traversal tweaks in the Accessibility layer. + * @returns A HybridSnapshot containing the combined tree, XPath map, URL map, and per-frame data. + */ + @logAction("Page.snapshot") + async snapshot(options?: SnapshotOptions): Promise