Playwright fixture — page.screenshot({ annot })
The @ingcreators/annot-product-docs package ships an extended
Playwright test fixture that intercepts page.screenshot()
and locator.screenshot(). When the call carries an
annot: { … } option, the fixture bundles four coordinated
steps into one:
- Refresh the MDX
annot:snapshotblock against the current page (viaproductDocs.syncfrom@ingcreators/annot-product-docs). - Take the raw screenshot.
- Resolve
<Overlay>annotations and merge with any caller-supplied inline overlays. - Compose the output PNG — editable wrap, flat raster, or
tags-only sidecar — and write to
path.
import { test } from "@ingcreators/annot-product-docs";
test("app overview", async ({ page }) => { await page.goto("https://annot.work/app/"); await page.screenshot({ path: "public/app/shots/app-overview.png", annot: { mdx: { id: "app-overview", path: "src/content/docs/app/index.mdx" }, tags: { source: "docs-tour", capturedAt: new Date().toISOString() }, }, });});Choosing your import
Section titled “Choosing your import”Two packages export test with the annot option:
| Import | Use when |
|---|---|
import { test } from "@ingcreators/annot-product-docs"; | You’re using the MDX-linked flow (annot.mdx) — e.g. living-product-docs tours. Also gets overlays / tags / editable. |
import { test } from "@ingcreators/annot-playwright"; | VRT specs, marketing screenshots, AI agent flows — anywhere annot.overlays / annot.tags is enough. Doesn’t drag in the MDX parser dependencies. |
Both surfaces share the same patch at runtime — the
@ingcreators/annot-product-docs chain registers an MDX-aware
resolver into @ingcreators/annot-playwright’s
annotSourceResolvers registry on module load. Plugin authors
can register their own resolvers (Figma / Sentry / custom data
sources) the same way.
The @ingcreators/annot-product-docs-astro/playwright subpath
that earlier docs referenced is a deprecated re-export; new code
should pick one of the two above. The deprecated subpath emits
a DeprecationWarning at import time and will be removed in
@ingcreators/annot-product-docs-astro@0.5.0.
The resulting PNG is drop-in re-editable in Annot Cloud
(annot.work/app/) — readers can save the image and load it as
an editable session, with the badges restored as selectable /
movable / colour-editable objects.
Pass-through to vanilla Playwright
Section titled “Pass-through to vanilla Playwright”page.screenshot() calls without an annot option are
unchanged. The fixture only kicks in when annot carries a
contribution (mdx / overlays / tags); shorthand values like
annot: true and annot: {} also pass through to the original
method. This means codegen tools — Playwright Codegen, Chrome
DevTools Recorder — emit standard page.screenshot({ path })
calls that work as-is. Adding annot: { mdx: { … } } by hand to
the codegen-generated skeleton is the standard editing flow.
The annot option — compositional fields
Section titled “The annot option — compositional fields”interface AnnotScreenshotOptions { /** Refresh the MDX `annot:snapshot` block and resolve the * `<Screen id>`'s overlays. */ mdx?: { id: string; path: string };
/** Caller-supplied annotations — merged with MDX-derived ones * when both are present. Same DSL `@ingcreators/annot-annotator` * accepts. */ overlays?: BboxAnnotation[];
/** Provenance metadata written verbatim into the PNG's XMP. * No auto-fill — the fixture writes exactly what's here. */ tags?: Record<string, string>;
/** Default `true`: annotations preserved as SVG layer + original * capture embedded → re-editable in Annot Cloud. `false`: * annotations baked into visible pixels, no XMP layer, no * embedded original — flat PNG, no round-trip. */ editable?: boolean;}Recipes
Section titled “Recipes”// 1. MDX-linked: refresh snapshot + bake overlays in one call.await page.screenshot({ path: "shot.png", annot: { mdx: { id: "login", path: "docs/login.mdx" } },});
// 2. Inline overlays: no MDX, caller provides annotations.await page.screenshot({ path: "shot.png", annot: { overlays: [ { type: "rect", bbox: { x: 10, y: 10, width: 200, height: 50 }, intent: "warning" }, { type: "numberedBadge", bbox: { x: 240, y: 12, width: 24, height: 24 }, number: 1 }, ], },});
// 3. MDX + extra inline overlay for this test run only.await page.screenshot({ path: "shot.png", annot: { mdx: { id: "checkout", path: "docs/checkout.mdx" }, overlays: [ { type: "rect", bbox: testFailureRegion, intent: "error" }, ], tags: { variant: testInfo.project.name }, },});
// 4. Provenance sidecar: tag a PNG with test metadata, no overlays.await page.screenshot({ path: testInfo.outputPath("failure.png"), annot: { tags: { source: "vrt-failure", testId: testInfo.titlePath.join(" / "), }, },});
// 5. Locked / non-re-editable: bake overlays into pixels.await page.screenshot({ path: "marketing/feature.png", annot: { mdx: { id: "feature", path: "marketing/feature.mdx" }, editable: false, },});Locator screenshots
Section titled “Locator screenshots”locator.screenshot({ annot }) works the same way, with one
addition: overlay coordinates are automatically rebased into
the locator’s clip. Overlays that fall outside the locator’s
bounding box are dropped with a warning (surfaced via
console.warn and Playwright’s test.info().annotations).
await page.locator("header").screenshot({ path: "header.png", annot: { overlays: [ // page-space bbox — rebased onto the header's clip { type: "rect", bbox: { x: 120, y: 60, width: 50, height: 30 } }, ], },});Page-space coordinates fit naturally: you describe overlays in
the coordinate system the snapshot uses (page pixels), and the
fixture translates per locator’s boundingBox().
When the locator has no bounding box (off-screen / hidden), the
fixture throws with a friendly diagnostic — re-test with a
stable selector or waitFor().
page.screenshot({ clip, annot }) honours clip the same way —
the explicit clip is treated as the rebase reference.
Output file shapes
Section titled “Output file shapes”| Input | Output |
|---|---|
annot absent / true / {} | Plain PNG (= vanilla Playwright) |
annot: { tags } only | Plain PNG + iTXt XMP chunk (no <annot:annotations>; editor treats as a normal PNG, external XMP tools read the tags) |
annot: { overlays | mdx } | Editable PNG (annotations preserved as SVG layer, original capture embedded) |
annot: { overlays | mdx, editable: false } | Flat PNG (annotations baked into pixels, no XMP layer) |
Tags are written verbatim. The fixture does not auto-fill
capturedAt / commit / etc — callers who want them write them.
Soft-convention key names (documented in
createAnnotator):
| Key | Meaning |
|---|---|
source | What produced the PNG (e.g. "docs-tour", "vrt-failure"). |
screen | Living-product-docs <Screen id> value. |
capturedAt | ISO timestamp. |
commit | Git SHA when applicable. |
A common docs-tour preamble:
tags: { source: "docs-tour", screen: SCREEN_ID, capturedAt: new Date().toISOString(), ...(process.env.GITHUB_SHA ? { commit: process.env.GITHUB_SHA } : {}),}Companion helpers
Section titled “Companion helpers”For callers that want to compose annotations themselves without
going through the annot option, the canonical homes export:
import { rebaseAnnotations, describeAnnotation,} from "@ingcreators/annot-playwright";import { resolveMdxAnnotations, svgFromBboxAnnotations,} from "@ingcreators/annot-product-docs";rebaseAnnotations(annotations, clip)— pureBboxAnnotation[]→{ kept, dropped }partition. Same function the patch uses internally.resolveMdxAnnotations({ mdxPath, screenId, dims })— read MDX overlays into a typed annotation list.svgFromBboxAnnotations(annotations)— wrap aBboxAnnotation[]into a single-root<svg>ready forAnnotator.toEditablePng()/toPng().
Third-party packages can extend the patch with their own
annot.* field by pushing a resolver into
annotSourceResolvers:
import { annotSourceResolvers } from "@ingcreators/annot-playwright";
annotSourceResolvers.push(async ({ annot, page }) => { if (!annot.figma) return null; return { prepare: () => refreshFigmaCache(annot.figma), resolveAnnotations: (dims) => readFigmaOverlays(annot.figma, dims), };});Codegen workflow
Section titled “Codegen workflow”Generate a Playwright spec skeleton with
npx playwright codegen <url> (or the VSCode “Record at cursor”
action). The codegen produces standard
await page.screenshot({ path: "…" }) calls. Add the import
swap and the annot: { … } opt by hand:
import { test, expect } from "@playwright/test";import { test, expect } from "@ingcreators/annot-product-docs";
test("…", async ({ page }) => { await page.goto("…"); await page.screenshot({ path: "shot.png" }); await page.screenshot({ path: "shot.png", annot: { mdx: { id: "…", path: "docs/….mdx" } }, });});If you don’t need MDX support, swap to
@ingcreators/annot-playwright instead — expect is
re-exported from both packages so the import swap is a
one-liner either way.