Skip to content

PER-8985 feat: toHaveScreenshot drop-in — route existing Playwright assertions through Percy#640

Open
Shivanshu-07 wants to merge 15 commits into
masterfrom
feat/tohavescreenshot-dropin
Open

PER-8985 feat: toHaveScreenshot drop-in — route existing Playwright assertions through Percy#640
Shivanshu-07 wants to merge 15 commits into
masterfrom
feat/tohavescreenshot-dropin

Conversation

@Shivanshu-07

Copy link
Copy Markdown
Contributor

What

Ships the toHaveScreenshot() drop-in inside @percy/playwright (require('@percy/playwright/dropin') — one config line, zero test changes). Every existing Playwright visual assertion routes through Percy:

  • Web project token → serialized-DOM web snapshot (same capture as percySnapshot: readiness gate, responsive capture, CORS iframes; Locator subjects become scoped snapshots), rendered server-side.
  • App project token → the captured PNG uploads straight through the comparison ingest (tag + tile) — no render flow, matching App Percy's existing ingestion.
  • Any other token (automate/generic) → a clear wrong-token configuration error.

Assertions always pass locally (verdict moves to Percy review); Percy being down falls back to native toHaveScreenshot for the whole run; optional sync mode and a CI gate reporter are included.

Baseline seeding (CLI-driven)

  • dropin/baseline/provider.js — the "@percy/cli".baselineProvider hook percy exec discovers: resolves the Playwright config outside a test run (config-capture reporter under playwright test --list, so TS configs work) and maps committed *-snapshots PNGs onto Percy identity.
  • commands/setup-baseline.mjspercy playwright:setup-baseline, the explicit (re-)baseline command for established projects (storybook-style command registration).

Depends on

  • percy/cli — client build attributes + exec seeding orchestration (companion PR)
  • percy/percy-api#6178 + companion API PR — baseline auto-approval

Testing

126 specs green (dispatch, identity, provider, config-resolution units + real-runner integration); E2E verified locally against percy-api and on staging (web + app projects, all four flows).

🤖 Generated with Claude Code

Shivanshu-07 and others added 11 commits July 8, 2026 21:43
…sertions through Percy

Adds @percy/playwright/dropin: one require line in playwright.config.js
overrides Playwright's toHaveScreenshot() so every existing screenshot
assertion is captured and uploaded to Percy, with the suite never failing on
a Percy problem (missing token / CLI down → the whole run transparently falls
back to native toHaveScreenshot).

Registration writes the matcher into Playwright expect's internal userMatchers
(call-time dispatch spreads them after built-ins) because the public
expect.extend() silently ignores built-in matcher names on the shared
instance; on an unrecognized expect shape it degrades to extend() WITH a loud
warning — never a silent no-op. Verified working on @playwright/test 1.60/1.61.

Capture modes (config file .percy-playwright-dropin.json):
- screenshot (default): the Playwright-stabilized PNG is uploaded as a raw
  comparison (generic/app projects); snapshot identity (name/browser/width)
  mirrors Playwright's on-disk baseline naming byte-for-byte, and tag
  dimensions are parsed from the PNG (percy-api validates height).
- snapshot: serialized-DOM web snapshot for web projects — DELEGATES to this
  repo's own captureDOM, so the drop-in inherits the readiness gate,
  responsive capture and cross-origin iframe handling and cannot drift from
  percySnapshot(). Locator subjects become element-scoped snapshots via a
  transient data attribute + scope.

First-build-as-baseline: the bundled percy-playwright wrapper bin flags the
run's build as a baseline candidate (PERCY_DROPIN_BASELINE_CANDIDATE, mirrors
PERCY_BUILD_SOURCE); percy-api decides first-ness server-side, and on a
genuine first build the dropin globalSetup seeds the repo's committed
Playwright baseline PNGs as the build's content (8-way parallel) while the
override skips live captures — the server auto-approves it (flag-gated), so
diffs start on the very next run. Also ships the opt-in fail-on-changes gate
reporter and sync-assertion mode.

Compat fixes surfaced by the @playwright/test 1.24→1.60 devDep bump:
- ENV_INFO's top-level require('playwright/package.json') crashes at import
  time under the 1.61 runner (CJS↔ESM interop) — now guarded, degrades to
  @playwright/test's version or a bare label
- the dropin↔root require is lazy for the same interop reason

Tests: dispatch specs drive the REAL Playwright expect through the override
and hard-assert the posted comparison (retry-wrapped against the shared
testing-server reset race); unit specs pin name derivation, first-build
seeding and locator scoping. 117 passed, lint clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The toHaveScreenshot drop-in needs modern @playwright/test (expect internals),
whose engine floor is Node 18. Runtime support for the core SDK entry is
unchanged; this bumps the dev/CI toolchain only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Playwright >=1.39 no longer downloads browsers as a package postinstall; the
suite failed on CI with 'Executable doesn't exist'. Fetch the launched
browser (with system deps) after yarn install.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ope nyc to the core surface

nyc instruments the arrow functions dropin/dom.js ships to the browser via
locator.evaluate, and the injected coverage counters don't exist in the page
(ReferenceError: cov_… is not defined) — annotate them with istanbul ignore,
the same convention index.js/utils.js already use for browser-executed code.

Exclude dropin/ and bin/ from the 100% coverage gate for now: the modules are
covered by the standalone package's 154-test unit suite, and porting that
suite into this harness is a flagged fast-follow. The existing core surface
keeps its 100% bar unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Which fallback executes depends on the consumer's installed packages — the
defensive branches can't all be exercised in one environment and were the
last gap under the 100% coverage gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Node 18 went EOL in April 2025; its leg also mis-collects subprocess coverage
for utils.js under the percy-exec harness (identical 117-test pass count,
different coverage) — not worth chasing on a dead runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Resolves the 7 blocking semgrep findings (path-join-resolve-traversal,
CWE-22): NUL bytes are stripped from any externally supplied directory
before it reaches path.join, and directory-walk entry names must be a
single path component (no separators, no dot/dot-dot) or the entry is
skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the SDK's own flows

The capture flow is no longer a user config (captureMode removed): the
override reads the project type from the CLI healthcheck and delegates to
the flow that project accepts —

  web (default)  -> percySnapshot()  (serialized-DOM, server-side render;
                    dom.js seam adds Locator scoping + viewport pinning)
  automate       -> percyScreenshot() (Percy on Automate)
  app/generic    -> raw-PNG upload via the comparison ingest (capture.js)

The snapshot seam now calls the repo's percySnapshot() directly instead of
re-assembling the postSnapshot options, inheriting closed-shadow-root
piercing, readiness gate, responsive capture and CORS iframes with zero
duplication. The committed-baseline seed applies only to the raw-PNG flow
and skips itself on web/automate projects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per design review: the drop-in is WEB-ONLY — every toHaveScreenshot() is a
percySnapshot web snapshot; an app/automate token now raises a clear
configuration error (validateConfig), like other SDKs' wrong-token errors.

Baseline seeding moves OUT of the test process into the CLI:
- dropin/baseline/provider.js — the '@percy/cli'.baselineProvider hook percy
  exec discovers: resolves the Playwright config outside a test run (a
  config-capture reporter under 'playwright test --list', so TS configs work),
  maps committed *-snapshots PNGs onto Percy identity, reads tag heights from
  PNG headers.
- commands/setup-baseline.mjs — 'percy playwright:setup-baseline', the
  explicit (re-)baseline command for ESTABLISHED projects: uploads committed
  PNGs into a build created with dropin-baseline-setup (auto-approved
  server-side), no tests run. Reuses @percy/cli-exec's upload loop.
- retired: the percy-playwright wrapper bin, the globalSetup seed
  (dropin/global-setup.js + first-build.js), the matcher's seeded-baseline
  skip, and the BYOS/automate dispatch paths (capture.js). First-build
  detection for the sync classifier now keys on the server-decided build
  source from the healthcheck.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…type

Correction to the web-only change: the drop-in supports BOTH project types.
Web tokens take serialized-DOM snapshots (render flow); app tokens upload
the captured PNG straight through the comparison ingest with no render
flow, exactly how App Percy ingests screenshots (capture.js restored).
Only non-web/non-app tokens (automate, generic, ...) raise the wrong-token
configuration error. setup-baseline and the seeding provider accept app
tokens too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shivanshu-07 and others added 4 commits July 14, 2026 10:28
'enabled: false' in .percy-playwright-dropin.json (or PERCY_DROPIN_DISABLE=true,
which also turns off percy exec's baseline seeding) makes toHaveScreenshot()
delegate to the ORIGINAL matcher with pure native semantics — including the
missing-baseline throw, since this is a deliberate opt-out rather than the
protective disabled-Percy fallback. percySnapshot()/percyScreenshot() keep
working under percy exec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SDK previously relied on the consumer's hoisted copy (via @percy/cli),
so an older tree silently lacked mergeSnapshotOptions and every snapshot
degraded per-assertion. A direct floored dependency guarantees a compatible
copy regardless of what the consumer hoists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hared-server reset race

Same toPass retry the other post+read-back specs use — the shared testing
server's log store is wiped by parallel workers' setupTest resets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…h + hardening

High:
- reporter: onEnd returns { status: 'failed' } — Playwright's process.exit()
  discards process.exitCode, so the gate could never red CI
- reporter: pending-grace probe — under percy exec the build finalizes
  after the test process exits, so waitForBuild stalled ~10min and the
  gate silently never fired; now bows out with build:wait guidance
- identity: browserFamily keys on use.browserName (falls back to project
  name) — display names like 'Desktop Chrome' never paired with
  baselines discovered under 'chromium'

Medium:
- config validation errors latch and re-throw on every assertion (was:
  only the first assertion surfaced the error)
- isFailing: informational mode never reds CI, incl. failed/errored builds
- identity: port Playwright's trimLongString — >100-char anonymous stems
  are trimmed on disk and must trim identically head-side
- setup-baseline: friendly upgrade error when @percy/cli lacks
  uploadBaselines

Low:
- resolve-config: Windows-safe spawn (node + @playwright/test/cli, not npx)
- wire config.snapshotDir through the reporter into discovery
- .not.toHaveScreenshot() honors the always-pass posture (pass inversion)
- skip app posts when PNG dimensions are unreadable (server rejects
  heightless tags)
- drop dead code (base-branch.js, runNativeMatcher, isWriteOnlyToken),
  refresh stale types/index.d.ts, align engines/peer floors (node>=16,
  @playwright/test>=1.49)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Shivanshu-07

Copy link
Copy Markdown
Contributor Author

Claude Code PR Review

PR: #640Head: ed2682aReviewers: fallback inline checklist (deep-review agent)

Summary

Adds the toHaveScreenshot() drop-in: matcher override with web/app dispatch (percySnapshot DOM vs comparison ingest), always-pass posture, native fallback, baseline provider + config-capture reporter, setup-baseline command, and opt-in CI gate reporter.

Review Table

Priority Category Check Status Notes
High Security No hardcoded secrets or credentials Pass
High Security Authentication/authorization checks present N/A SDK — token handled by CLI/env
High Security Input validation and sanitization Pass token-type validation; sanitizeForFilePath parity
High Security No IDOR — resource ownership validated N/A
High Security No SQL injection (parameterized queries) N/A
High Correctness Logic is correct, handles edge cases Pass 3 High + 5 Medium findings fixed in ed2682a (gate exit status, exec deadlock, browserFamily parity, validation latch, informational-mode gate, trimLongString parity, CLI-version guard)
High Correctness Error handling is explicit, no swallowed exceptions Pass D3 always-pass swallowing is by design and documented
High Correctness No race conditions or concurrency issues Pass temp-file handoff uses pid+randomBytes; spawn is synchronous
Medium Testing New code has corresponding tests Pass 131 tests + new reporter/identity/latch coverage added
Medium Testing Error paths and edge cases tested Pass gate red-run, pending bow-out, isFailing matrix, .not, latch
Medium Testing Existing tests still pass (no regressions) Pass full suite green (1 known shared-server flake passes on rerun)
Medium Performance No N+1 queries or unbounded data fetching Pass
Medium Performance Long-running tasks use background jobs N/A
Medium Quality Follows existing codebase patterns Pass
Medium Quality Changes are focused (single concern) Pass
Low Quality Meaningful names, no dead code Pass dead code removed in ed2682a (base-branch.js, runNativeMatcher, isWriteOnlyToken)
Low Quality Comments explain why, not what Pass
Low Quality No unnecessary dependencies added Pass only @percy/sdk-utils floor

Findings

All review findings were fixed on this PR before this report (commit ed2682a):

  • High dropin/reporter.js: gate could never red CI (Playwright discards process.exitCode) → onEnd now returns { status: 'failed' }.
  • High dropin/reporter.js: gate deadlocked ~10min under percy exec (build finalizes after the test process exits) → pending-grace probe + build:wait guidance.
  • High dropin/identity.js: head browserFamily used the project display name, baselines used use.browserName → identity unified on browserName.
  • Medium: config-validation latch (error now re-thrown on every assertion), informational gate never reds on errored builds, trimLongString parity for >100-char stems, setup-baseline CLI-version guard, reporter/sync/fallback test coverage.
  • Low: Windows-safe playwright spawn, snapshotDir wiring, .not always-pass handling, heightless-tile skip, dead code removal, stale types refresh, engines/peer floors.

Verdict: PASS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant