@s4labs/pixmoat-playwright
v0.1.4
Published
Playwright integration for pixmoat visual regression testing
Readme
@s4labs/pixmoat-playwright
Playwright integration for pixmoat visual regression testing.
Installation
npm install @s4labs/pixmoat-playwrightUsage
Fixture API (recommended)
Capture pages explicitly with the pixmoat fixture. Start the build in globalSetup and finish it in globalTeardown:
// global-setup.ts
import { writeFileSync } from "node:fs";
import { startBuild } from "@s4labs/pixmoat-playwright";
export default async function globalSetup() {
const buildId = await startBuild();
// Hand the build ID to the CI gate step (see "CI gate" below).
writeFileSync("pixmoat-build.env", `PIXMOAT_BUILD_ID=${buildId}\n`);
}// global-teardown.ts
import { finishBuild } from "@s4labs/pixmoat-playwright";
export default async function globalTeardown() {
await finishBuild();
}// tests/visual.spec.ts
import { test } from "@s4labs/pixmoat-playwright";
test("visual check", async ({ page, pixmoat }) => {
await page.goto("/my-page");
await pixmoat.snapshot(page, "my-page");
});// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
globalSetup: "./global-setup.ts",
globalTeardown: "./global-teardown.ts",
});finishBuild() returns Promise<void>. It marks the build complete and logs [pixmoat] Build finished: …, but it does not fail CI when the build has unreviewed changes. Add the separate gate step below.
Required environment variables:
PIXMOAT_PROJECT— Project slugPIXMOAT_TOKEN— Project access token (pxg_...)PIXMOAT_BRANCH— Git branch namePIXMOAT_COMMIT— Git commit SHA
Optional environment variables:
PIXMOAT_URL— Base URL of the pixmoat SaaS (default:https://app.pixmoat.com)PIXMOAT_BUILD_ID— Reuse an existing build instead of creating onePIXMOAT_CI_URL— CI job URLPIXMOAT_PR— Pull request number
Section and dialog captures
Use captureSelector to capture a section on its real page while retaining
its width, theme, and responsive styles:
import { test } from "@s4labs/pixmoat-playwright";
test("payment method", async ({ page, pixmoat }) => {
await page.goto("/settings/billing");
await page.locator('[data-testid="payment-method"]').scrollIntoViewIfNeeded();
await pixmoat.snapshot(page, "Payment method - Detail", {
captureSelector: '[data-testid="payment-method"]',
viewports: ["1440x900", "375x812"],
});
});The selector must match one visible element. Its visible bounds, including
ancestor scroll-container clipping, define the image. For tall sections,
prepare named scroll states to cover the header and footer separately.
captureSelector overrides fullPage; viewport, browser, and DPR still
identify the rendering environment. Element maps and overlay coordinates
use the cropped image origin. Accessibility and performance audits still
cover the page; violations outside the crop retain their findings without
an image overlay.
Prepare layout-sensitive states independently at each viewport. Capture portalled dialogs by their own selector. Keep representative page captures for section order, spacing, and dialog placement. Changing an existing capture's scope requires a distinct name and a reviewed baseline migration.
captureSnapshotImage(page, fullPage, captureSelector) exposes the same image
capture for applications that write local evidence without uploading.
It performs no Pixmoat requests. Callers own page preparation and settling.
CI gate
Run a separate step after npx playwright test that reads the build verdict and fails unless it is PASS:
npx playwright test
. ./pixmoat-build.env # sets PIXMOAT_BUILD_ID, written by global-setup.ts
verdict=$(curl --fail --silent --show-error --max-time 330 \
-H "Authorization: Bearer $PIXMOAT_TOKEN" \
"${PIXMOAT_URL:-https://app.pixmoat.com}/api/builds/$PIXMOAT_BUILD_ID/agent-summary?wait=true&timeout=300" \
| jq -r '.verdict')
echo "pixmoat verdict: $verdict"
test "$verdict" = "PASS"wait=true waits for the server to finish processing the build (up to timeout seconds, default 120, max 300). It does not wait for a human review. The step fails while changes are pending review (REVIEW, REVIEW_NEW), when a run was rejected (FAIL), and when processing times out (HTTP 408). After a reviewer approves the build in pixmoat, retry the step.
GitLab CI Integration
Fragment. The complete pipeline, with the workflow rules and variables, is in the GitLab CI guide. PIXMOAT_TOKEN is a masked, unprotected CI/CD variable.
variables:
PIXMOAT_PROJECT: "my-project"
visual-regression:
stage: test
image: mcr.microsoft.com/playwright:v1.52.0-noble # match your @playwright/test version
variables:
PIXMOAT_BRANCH: $CI_COMMIT_REF_NAME
PIXMOAT_COMMIT: $CI_COMMIT_SHA
script:
- npm ci
- npx playwright test
artifacts:
when: always
reports:
dotenv: pixmoat-build.env # PIXMOAT_BUILD_ID for the gate job
visual-gate:
stage: test
needs:
- job: visual-regression
artifacts: true
image: alpine:3.20
script:
- apk add --no-cache curl jq
- |
verdict=$(curl --fail --silent --show-error --max-time 330 \
-H "Authorization: Bearer $PIXMOAT_TOKEN" \
"${PIXMOAT_URL:-https://app.pixmoat.com}/api/builds/$PIXMOAT_BUILD_ID/agent-summary?wait=true&timeout=300" \
| jq -r '.verdict')
echo "pixmoat verdict: $verdict"
test "$verdict" = "PASS"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"Playwright Reporter (beta)
Warning: the reporter uploads only the PNG attachments it sees on test results. A passing
toHaveScreenshot()assertion produces no attachment, and failed uploads are not accounted for, so snapshots it omits can be reported as removals. Approving those removals retires valid baselines. Use the fixture API for CI until the reporter can account for every expected snapshot.
// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
reporter: [
["@s4labs/pixmoat-playwright/reporter", {
// Optional: override env-var configuration
url: "http://localhost:3000",
project: "my-project",
token: "pxg_...",
}]
],
});The reporter reads the same environment variables as the fixture, plus:
PIXMOAT_FAIL_ON_BLOCKED— Fail the run when ci_status is "blocked" (default: "true")PIXMOAT_REPORT_DIR— Directory for CI report files (default: "pixmoat-report")
The reporter finishes the build itself. Don't also call finishBuild() in globalTeardown: the second finish request is rejected with HTTP 409.
CI Reports
The reporter generates three report files for CI integration:
pixmoat.env — dotenv format for GitLab
artifacts:reports:dotenv:PIXMOAT_REVIEW_URL=https://pixmoat.example.com/p/my-project/builds/abc123 PIXMOAT_BUILD_ID=abc123 PIXMOAT_CI_STATUS=passedjunit.xml — JUnit XML test results for GitLab
artifacts:reports:junit. Contains per-screenshot testcases when agent-summary data is available, or aggregate testcases by count bucket otherwise.report.html — self-contained HTML report with a build status badge, a counts table, a per-screenshot table (when available) and a link to the review URL.
GitLab example using the reporter's report files:
visual-regression:
stage: test
script:
- npm ci
- npx playwright test
artifacts:
when: always
reports:
dotenv: pixmoat-report/pixmoat.env
junit: pixmoat-report/junit.xml
expose_as: "Visual Report"
paths:
- pixmoat-report/report.html
expire_in: 30 days
environment:
name: review/$CI_COMMIT_REF_SLUG
url: $PIXMOAT_REVIEW_URLThis configuration:
- Exposes the review URL as an environment button in the merge request
- Displays test results in the merge request test summary
- Provides an HTML report artifact for download
Reporter Options
// Fragment: one entry of the reporter array in playwright.config.ts
["@s4labs/pixmoat-playwright/reporter", {
// Server config
url: "http://localhost:3000",
project: "my-project",
token: "pxg_...",
// Build metadata
branch: "main",
commitSha: "abc123",
ciUrl: "https://ci.example.com/jobs/123",
prNumber: 42,
// Behavior
failOnBlocked: true,
// CI reports
reportDir: "pixmoat-report",
}]Writing CI Reports Manually
You can also generate CI reports programmatically:
import { writeCiReports, type BuildSummary } from "@s4labs/pixmoat-playwright";
const summary: BuildSummary = {
build_id: "abc123",
status: "uploaded",
ci_status: "passed",
counts: { total: 10, new: 2, unchanged: 7, diff: 1, missing: 0, removed: 0 },
web_url: "https://pixmoat.example.com/p/my-project/builds/abc123",
};
await writeCiReports({
summary,
reviewUrl: summary.web_url || "https://...",
metadata: {
project: "my-project",
branch: "main",
commitSha: "abc123",
},
outputDir: "pixmoat-report",
});Upload overload retries
Snapshot uploads and unchanged-image confirmations retry explicit HTTP 429 and 503 responses, up to four attempts total. Retries use exponential backoff with jitter and honor Retry-After (seconds or HTTP date). A requested delay above ten seconds, or beyond the operation's remaining time budget, stops automatic retries and surfaces the response.
The client's requestTimeoutMs covers the entire upload/confirmation operation, including attempts and backoff. Repeated attempts reuse the same build, snapshot identifiers, metadata and image bytes. Other mutations, including build creation and finishing, are not automatically retried. Network failures and timeouts have ambiguous outcomes and are not retried; neither are authentication failures, oversized-image responses (413), other client errors, or other server errors. Reduce oversized images or address the reported error instead of retrying them unchanged.
AI Agent Integration
The pixmoat MCP server (pixmoat mcp) and the agent skill both need the pixmoat CLI, which doesn't have a public install path yet. Until it does, an agent can read a build's verdict with the same agent-summary request as the CI gate. The digest format agents read is in the agent digest reference.
License
MIT
GitLab merged-result captures
Merged-result CI jobs automatically record source, target and captured commit identities. The client checks Git HEAD before creating a new build. The server verifies the declared pair through the configured GitLab connection and compares only with frozen target visual baselines. The target needs a complete accepted capture for its exact commit.
Integration captures fail when stale, unverifiable, or awaiting review, even in report-only mode. Reporter upload and finish errors also fail the job, regardless of PIXMOAT_FAIL_ON_BLOCKED. Merge trains are rejected for now. Ordinary branch and local captures keep their existing behavior. See the GitLab guide for setup and limits.
