Let Playwright or Puppeteer create the browser state. Run SnapDOM inside that page, then return result.url or a raster data URL to Node. A same-origin iframe can be captured from its parent; a cross-origin document must be served from the same origin or captured from its own page or frame context. useProxy can fetch assets, but it cannot grant access to iframe DOM.
Run the capture where the DOM lives
SnapDOM starts from an Element. It reads layout, computed styles, fonts and image state, then asks the browser to paint the styled clone inside an SVG <foreignObject>. A Node process, server renderer or worker without a document cannot provide those browser APIs.
The process that receives an HTTP request can still own the job. It launches a browser, prepares the page and saves the output. The call to snapdom() stays inside the page that owns the element.
| Situation | Capture context | Route |
|---|---|---|
| SSR app after hydration | The client page | Capture after mount and after the DOM is ready |
| Backend Node job | A controlled browser page | Launch Playwright or Puppeteer and inject SnapDOM |
| Web Worker | The page's main thread | Send a capture request with postMessage |
| Service Worker or edge worker | A separate browser service | Forward the job to a real browser runtime |
| Cross-origin document | Its own page or frame context | Run the capture where that document is readable |
| Navigation or E2E flow | The final, stable page state | Let the browser driver navigate, interact and wait first |
jsdom and similar DOM implementations can parse markup, but they do not replace browser layout and paint. They cannot supply the geometry, font rendering, SVG decode and canvas behavior that a faithful capture needs.
Give Node a real browser
This Playwright script loads the installed IIFE bundle, opens a report, waits for an application-owned ready signal and makes two files from one SnapDOM capture. Node manages the process and filesystem. The page creates the SVG and PNG.
npm install @zumer/snapdom playwright npx playwright install chromium
import { chromium } from 'playwright'
import { writeFile } from 'node:fs/promises'
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const snapdomBundlePath = require.resolve('@zumer/snapdom')
const browser = await chromium.launch()
try {
const context = await browser.newContext({
bypassCSP: true,
viewport: { width: 1280, height: 720 },
deviceScaleFactor: 1
})
const page = await context.newPage()
await page.goto('https://example.com/report', {
waitUntil: 'domcontentloaded'
})
await page.locator('#report[data-export-ready="true"]').waitFor()
await page.addScriptTag({ path: snapdomBundlePath })
const output = await page.evaluate(async () => {
await document.fonts.ready
const report = document.querySelector('#report')
if (!report) throw new Error('Report element not found')
const capture = await window.snapdom(report, {
embedFonts: true,
dpr: 1
})
const png = await capture.toPng()
return { svg: capture.url, png: png.src }
})
const svgBody = output.svg.slice(output.svg.indexOf(',') + 1)
const pngBody = output.png.slice(output.png.indexOf(',') + 1)
await Promise.all([
writeFile('report.svg', decodeURIComponent(svgBody)),
writeFile('report.png', Buffer.from(pngBody, 'base64'))
])
} finally {
await browser.close()
}
The package's require export resolves to the IIFE that exposes window.snapdom. Resolving the installed package is robust across hoisted and standalone installs, keeps CI output reproducible and also works offline. In Playwright, CSP bypass belongs to the browser context; Puppeteer uses page.setBypassCSP(true) before navigation.
CaptureResult, Blob, Canvas and HTMLImageElement instances stay in the browser process. Strings cross the page.evaluate() boundary cleanly, so produce every required format from the reusable capture and return data URLs or encoded text.
page.screenshot() is the direct tool for a single pixel-based screenshot of a page. Keeping SnapDOM inside the browser is useful when the application also exports client-side, when you need SVG, when a plugin edits the captured DOM, or when one frozen capture must feed several formats.
Capture cross-origin documents from their own context
When an iframe is same-origin, SnapDOM can read its document and place a PNG of the iframe's visible viewport into the parent capture. A sandboxed frame without allow-same-origin may still be inaccessible even when its URL looks local.
For a cross-origin iframe, the parent page cannot read iframe.contentDocument. SnapDOM leaves a striped placeholder by default and emits a warning; placeholders: false keeps an invisible spacer with the same box size. That behavior follows the browser's same-origin policy.
If you control the deployment, serve or reverse-proxy the document under the same origin. For an authorized third-party page, navigate a controlled browser directly to that URL, inject SnapDOM after navigation and capture document.documentElement. Playwright and Puppeteer can also run code in an individual cross-origin frame context:
const frame = page.frames().find(candidate =>
candidate.url().startsWith('https://partner.example/')
)
if (!frame) throw new Error('Partner frame not found')
await frame.addScriptTag({ path: snapdomBundlePath })
const framePng = await frame.evaluate(async () => {
await document.fonts.ready
const image = await window.snapdom.toPng(
document.documentElement,
{ clip: 'viewport', dpr: 1 }
)
return image.src
})
The driver reaches the frame through the browser automation protocol; SnapDOM still runs inside the document it reads. If the final artifact needs both the parent page and the child frame, capture the child separately, provide its data URL as an image in a controlled render state, then capture the parent.
If the required result is one PNG of the complete parent page, page.screenshot() is shorter and already includes the pixels composited for a cross-origin frame.
useProxy solves a different problem. It helps SnapDOM fetch cross-origin images, fonts and backgrounds that allow proxying. It does not make iframe DOM same-origin, and an open proxy creates its own authentication and server-side request risks.
Navigation stays with the browser driver
SnapDOM captures the state it receives. It does not open URLs, authenticate, click controls, wait for application data, manage test baselines or compare pixels. Playwright or Puppeteer owns those steps, then calls SnapDOM after the target state is stable.
await page.goto(appURL, { waitUntil: 'domcontentloaded' })
await page.getByRole('button', { name: 'Generate report' }).click()
await page.locator('#report[data-export-ready="true"]').waitFor()
await page.addScriptTag({ content: snapdomBundle })
const captureURL = await page.evaluate(async () => {
await document.fonts.ready
const report = document.querySelector('#report')
return (await window.snapdom(report, { dpr: 1 })).url
})
An explicit application signal is more reliable than a fixed delay or network-idle heuristic. A page can finish its requests while fonts, transitions, charts or asynchronous rendering are still changing what the user sees.
For visual regression, the runner should also fix the browser build, viewport, DPR, fonts, locale and timezone, and disable animations. SnapDOM supplies the artifact; the test system still owns baselines, thresholds and assertions.
Account for the browser service
A backend capture still pays for a browser process. Reuse a browser when jobs are frequent, isolate work in fresh contexts, cap concurrency and apply timeouts. A public endpoint that accepts arbitrary URLs also needs URL allowlists and network isolation so it cannot become a route into private services.
Every full navigation replaces the page's JavaScript context, so inject the bundle after the final navigation. Wait for the application's own export-ready state and document.fonts.ready, call SnapDOM inside page.evaluate(), and return result.url or a raster image's .src. Keep DOM objects inside the browser process.
Put SnapDOM inside the page
The complete reference includes the server-side recipe, capture API and plugin hooks used in this integration.
Open llms-full.txtRead the API