Skip to content

Repository files navigation

WidthWatch

Continuous responsive visual regression testing. WidthWatch finds failures between the three breakpoints everybody remembered to screenshot.

Run a correctness-first full-page scan from npm:

npx widthwatch https://example.com \
  --output widthwatch.html \
  --json widthwatch.json

For a pinned CI dependency, use npm install --save-dev widthwatch. If Chromium is not already available in the runner, install the matching browser once with npx playwright install chromium.

Initialize a versioned TypeScript config and a read-only reusable GitHub Actions workflow:

npx widthwatch init

The engine returns versioned TypeScript objects, renders a portable interactive HTML report, and compares a candidate page with a baseline at matching widths. Its adaptive sampler discovers where geometry changes and spends the screenshot budget around those intervals instead of pretending that mobile/tablet/desktop are the whole responsive surface.

What exists in v0.4

  • adaptive 320–1440px width timeline;
  • two-pass visual scanning: fast geometry discovery followed by bounded full-page evidence capture;
  • document and element overflow detection;
  • clipped-text and material leaf-overlap detection;
  • layout discontinuity signals;
  • observed issue ranges such as 742–811px, with adjacent clean samples, instead of claiming unsampled boundaries;
  • full-page visual mode with bounded scroll sweep and lazy-content activation;
  • optional reload-per-width and application-specific Playwright readiness hook;
  • optional compact JPEG evidence for standalone diagnostics;
  • PNG pixel comparison with candidate/baseline regression output;
  • CLI, TypeScript API and native standalone HTML reporter;
  • widthwatch.config.ts plus a generated reusable GitHub workflow;
  • product website, documentation and bounded public-scanner UI;
  • shareable interactive online reports for completed public scans;
  • protected AWS App Runner API with DNS-pinned egress, quotas and a one-instance hard cap;
  • GitHub Actions for CI, Pages, npm trusted publishing and AWS OIDC deployment.

TypeScript API

import {
  scanAtReportSchedule,
  scanResponsive,
  compareReports,
  generateHtmlReport,
  type WidthWatchReport,
} from "widthwatch";

const candidate: WidthWatchReport = await scanAtReportSchedule(
  "http://localhost:4173",
  baseline,
  {
    pageReady: (page) => page.waitForSelector("[data-app-ready]"),
    readinessKey: "app-ready-v1",
  },
);

const comparison = compareReports(baseline, candidate, {
  maxDiffRatio: 0.002,
  includeDiffImages: true,
});

const html = generateHtmlReport(comparison);

Pull-request use

Build or deploy the candidate in CI and keep a trusted main-branch JSON baseline. The CLI reads that baseline before scanning and captures the candidate at exactly the same widths:

npx widthwatch "$CANDIDATE_URL" \
  --baseline .widthwatch/home.json \
  --json artifacts/home.json \
  --output artifacts/home.html \
  --fail-on-regression

Baseline and candidate must run in the same pinned browser container. Browser rendering can vary by operating system, fonts, browser version and other host details; a visual threshold cannot compensate for unrelated environments.

The CLI uses correctness-first visual capture and reloads each width. Adaptive visual scans probe up to 24 widths geometrically, then use --max-captures (default 8) to bound the expensive full-page evidence schedule. Every discovery finding remains in report.probes and the canonical report.issues, even when its width has no screenshot. Baseline comparisons reproduce both the probe schedule and the evidence schedule. For a fast diagnostic probe that only inspects the viewport, use --layout-only; add --reload-per-width when a layout-only application calculates responsive state only during startup. --full-page is already the visual default; combined with --layout-only, it captures the whole document without enabling the visual scroll sweep.

Severity changes are explicit comparison outcomes. A finding that moves from warning to error enters both regressions and escalated; a lower severity is recorded in deescalated without being misreported as resolved. See the migration notes before reusing baselines created before v0.3.1.

The generated workflow is deliberately read-only and uploads the portable report as an artifact. It accepts a deployed preview URL through workflow_call or manual dispatch, and never uses pull_request_target. Connect it to the step that already deploys your application preview.

See the two-pass proof with 12 probes and 4 evidence captures, including discovery-only findings that remain actionable. The exact-width proof remains available for the baseline/candidate/diff path.

Repository

apps/web                 GitHub Pages site, docs and live demo client
apps/api                 bounded public API and pinned egress proxy
packages/widthwatch      npm engine, CLI, types, comparator and reporter
docs                     architecture and OpenAPI contract
infra/aws                App Runner, CloudFront and WAF infrastructure

Local development

Node.js 22 or newer and Chromium are required. The complete monorepo and the published package are both verified on Node.js 22 and 24.

npm ci
npx playwright install chromium
npm run lint
npm run format:check
npm run typecheck
npm test
npm run coverage
npm run dev

Coverage gates run per workspace. The hosted API additionally enforces focused thresholds for HTTP admission, public-target policy, pinned egress, byte allowance and hosted execution; the web client is measured in Chromium against its original TypeScript source.

Run the API separately:

npm run build
npm start --workspace @widthwatch/api

Public demo limits

The hosted surface is intentionally not the local package in the cloud. It accepts one credential-free public page, uses 5 geometry probes with compact JPEG captures, blocks media, caps navigation at 15 seconds and 200 requests, admits at most three queued jobs, and runs one browser. Every job gets a fresh bounded egress session: 10 MiB per plain-HTTP response, 25 MiB per opaque HTTPS tunnel and 75 MiB of transferred payload in total. Exhausting any allowance closes proxy sockets, aborts Chromium and fails the job closed. Job status remains in memory for 30 minutes; when AWS_INSTANCE_ROLE_ARN is configured, completed HTML reports are stored in a private encrypted S3 bucket with a 7-day lifecycle. Anyone who has the public report link can read its screenshots and page content until it expires; do not scan sensitive pages. The admission request streams lightweight heartbeats while the browser works so App Runner does not throttle detached CPU, while status polling returns lightweight metadata rather than embedded screenshots. Separate client, target, global, CloudFront/WAF, transfer and compute limits prevent arbitrary scale-out. The local visual package uses 24 discovery probes, up to 8 lossless evidence captures by default, and exact schedules for CI comparison.

See architecture, OpenAPI, security policy, and the public scanner incident runbook.

Deployment bootstrap

  1. Deploy infra/aws/github-deploy-role.yml, then the independent infra/aws/scanner-switch-iam.yml, reusing the account-level GitHub OIDC provider. Existing installations can update only the switch IAM stack; it gives the application deploy role read-only access to scanner state without recreating App Runner roles.
  2. Apply the GitHub supply-chain runbook: protect main and v*, restrict actions to pinned selected references, and configure the production and read-only monitoring environments.
  3. Add repository variables AWS_ACCOUNT_ID, AWS_DEPLOY_ROLE_ARN, AWS_CLOUDFORMATION_ROLE_ARN, AWS_ECR_ACCESS_ROLE_ARN, AWS_INSTANCE_ROLE_ARN, AWS_SCANNER_SWITCH_ROLE_ARN, AWS_SCANNER_SWITCH_EXECUTION_ROLE_ARN, AWS_CANARY_ROLE_ARN, and VITE_API_URL.
  4. Add WIDTHWATCH_ORIGIN_VERIFY_TOKEN and WIDTHWATCH_BUDGET_ALERT_EMAIL as environment secrets. Deployment fails closed when alert routing is absent.
  5. Bootstrap infra/aws/scanner-switch.yml once in us-east-1 with its default disabled parameter and the dedicated switch execution role. The application deploy refuses to run while this stack is absent and can only read its ARN.
  6. Confirm both regional SNS email subscriptions, then explicitly enable admission with the scanner switch runbook. There is no enabled fallback.
  7. Configure npm trusted publishing for repository damianociarla/widthwatch, workflow release.yml, environment production.
  8. Enable GitHub Pages with GitHub Actions as its source.

A v* tag validates, deploys the API, publishes npm, creates the GitHub Release and then publishes that exact released tag to the website. A manual recovery dispatch can resume an existing immutable tag after provider-side run loss; it cannot release mutable main. Actions are pinned to full SHAs and maintained by Dependabot. AWS credentials are short-lived through GitHub OIDC; the scheduled canary assumes a separate read-only role, while npm trusted publishing also uses OIDC and emits provenance.

The Deploy API workflow can bootstrap or redeploy only the hosted scanner without publishing an npm release.

License

MIT © Damiano Ciarla

About

Continuous responsive visual regression testing for modern web teams

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages