Skip to content

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:

  1. Refresh the MDX annot:snapshot block against the current page (via productDocs.sync from @ingcreators/annot-product-docs).
  2. Take the raw screenshot.
  3. Resolve <Overlay> annotations and merge with any caller-supplied inline overlays.
  4. 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() },
},
});
});

Two packages export test with the annot option:

ImportUse 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.

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.

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;
}
// 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.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.

InputOutput
annot absent / true / {}Plain PNG (= vanilla Playwright)
annot: { tags } onlyPlain 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):

KeyMeaning
sourceWhat produced the PNG (e.g. "docs-tour", "vrt-failure").
screenLiving-product-docs <Screen id> value.
capturedAtISO timestamp.
commitGit 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 } : {}),
}

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) — pure BboxAnnotation[]{ kept, dropped } partition. Same function the patch uses internally.
  • resolveMdxAnnotations({ mdxPath, screenId, dims }) — read MDX overlays into a typed annotation list.
  • svgFromBboxAnnotations(annotations) — wrap a BboxAnnotation[] into a single-root <svg> ready for Annotator.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),
};
});

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.