npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

playwright-render-contract

v0.1.1

Published

Assert runtime, readiness, structure, and viewport invariants in Playwright.

Readme

playwright-render-contract

Assert that a Playwright page is ready, structurally sound, free of runtime errors, and contained within its viewport. These checks complement Playwright screenshot assertions: a screenshot compares how the page looks against a baseline, and a render contract catches readiness, runtime, structure, and viewport problems that a picture can miss.

playwright-render-contract is a rule layer for an existing Playwright test suite. It does not launch browsers, host a site, mock APIs, or replace the Playwright runner.

Install

npm install --save-dev playwright-render-contract @playwright/test

Node.js 22 or newer and Playwright 1.58–1.x are supported. v0.1 is ESM-only and officially exercises Chromium.

Quick start

Install the observer before navigation so it can capture startup failures:

import { expect, test } from "@playwright/test";
import {
  attachRenderReport,
  defineRenderContract,
  observePage,
  renderContractMatchers
} from "playwright-render-contract";

expect.extend(renderContractMatchers);

const contract = defineRenderContract({
  ready: {
    all: [{ selector: "[data-ready=true]", state: "visible" }],
    none: [{ selector: "[aria-busy=true]", state: "visible" }],
    timeoutMs: 5_000
  },
  runtime: {
    pageErrors: "error",
    unhandledRejections: "error",
    consoleErrors: "error"
  },
  structure: {
    main: { min: 1, max: 1 },
    visibleH1: { min: 1, max: 1 },
    uniqueIds: "error"
  },
  viewport: {
    horizontalOverflow: "error",
    tolerancePx: 1
  }
});

test("page satisfies its render contract", async ({ page }, testInfo) => {
  const observer = await observePage(page);
  try {
    await page.goto("http://localhost:3000/");
    await expect(observer).toPassRenderContract(contract);

    const report = await observer.inspect(contract);
    await attachRenderReport(report, testInfo);
  } finally {
    await observer.dispose();
  }
});

The matcher receives the observer, not the page. This lifecycle is deliberate: creating an observer after page.goto() can miss an early exception, rejection, or console.error.

Working with screenshot checks

Screenshot assertions and render contracts answer different questions about the same page. A screenshot compares appearance against a baseline; a render contract checks whether the page settled, ran without runtime errors, and holds its expected structure and width. Run both against the same page and read the results together:

| Screenshot | Render contract | Reading | | --- | --- | --- | | Unchanged | Passes | Appearance matches the baseline and the readiness, runtime, structure, and viewport checks hold. | | Unchanged | Fails | A visually silent regression—an early runtime error, a missing landmark, duplicate IDs, or overflow the baseline did not reveal. | | Changed | Passes | The difference is visual only; the contract finds no readiness, runtime, structure, or viewport fault. | | Changed | Fails | Both kinds of regression are present: appearance moved and a contract check broke. |

Example: staged loading

Some pages reach their final appearance in stages. An interactive map, for instance, may first paint low-detail tiles and then swap in detailed ones; a screenshot taken too early captures the coarse frame. This models that class of problem and does not imply integration with any particular mapping service.

Let the contract gate the screenshot on a settled state. The recommended order is:

  1. create the observer before navigation, so startup failures are captured;
  2. navigate, then wait for the meaningful settled marker and the absence of any busy or low-quality state;
  3. take the screenshot once the contract passes.
const observer = await observePage(page);
try {
  await page.goto("http://localhost:3000/map");
  await expect(observer).toPassRenderContract({
    ready: {
      all: [
        {
          selector: 'main[data-render-state="settled"][data-map-ready="true"]',
          state: "visible"
        }
      ],
      none: [
        { selector: 'main[aria-busy="true"]', state: "visible" },
        { selector: '[data-tile-quality="low"]', state: "visible" }
      ],
      timeoutMs: 15_000
    }
  });
  await expect(page).toHaveScreenshot("map-settled.png");
} finally {
  await observer.dispose();
}

The application owns what the settled marker means and when it is set. If the marker is raised before the detailed tiles are in place, the contract passes early and the screenshot is still premature: the library waits for the marker but cannot correct one set too soon. The full progressive-rendering example shows this end to end.

Default contract

Calling observer.inspect() with no options checks:

  • document.readyState === "complete";
  • no page errors, unhandled rejections, or console.error;
  • exactly one <main>;
  • exactly one visible H1;
  • unique IDs;
  • no document-level horizontal overflow beyond one pixel.

Findings carry a severity. An "error" finding sets report.ok to false and fails toPassRenderContract; a "warning" finding is still reported but leaves the contract passing. The runtime channels, uniqueIds, and horizontalOverflow accept "error", "warning", or "off". Readiness timeouts, out-of-range main and visibleH1 counts, and incomplete-audit findings are always errors.

Readiness

ready: {
  document: "interactive",
  all: [
    { selector: "[data-shell-ready]", state: "attached" },
    { selector: "[data-content-ready]", state: "visible" }
  ],
  none: [
    { selector: "[aria-busy=true]", state: "visible" },
    { selector: "[data-state=error]", state: "attached" }
  ],
  timeoutMs: 3_000
}

Readiness uses selector and document predicates with Playwright timeouts; the library adds no fixed delay of its own. Selectors are CSS selectors and are validated before use: invalid CSS raises a configuration error immediately rather than waiting out the timeout and reporting a readiness failure.

Runtime errors

runtime: {
  pageErrors: "error",
  unhandledRejections: "error",
  consoleErrors: "warning",
  ignore: [
    "known optional integration",
    /^ResizeObserver loop limit exceeded$/
  ]
}

Events are collected from observer creation until inspect(). When Chromium reports one failure as both a page error and an unhandled rejection, the two land next to each other; if their normalized messages match and they are adjacent—no more than one sequence step apart—they are paired one to one and the duplicate page error is dropped so the failure is counted once. When the match is ambiguous or the entries are not adjacent, both findings are kept rather than risk hiding a real error. maxRuntimeEvents bounds memory if a page emits continuously:

const observer = await observePage(page, { maxRuntimeEvents: 500 });

Reaching the limit produces an error finding because the audit is incomplete. Duplicate-ID collection is likewise bounded to 10,000 tracked IDs and 100 individual findings by default. Tune maxTrackedIds and maxDuplicateIdFindings when auditing unusually large documents; reaching either limit produces an incomplete-audit error finding.

Unhandled promise rejections are classified through a Chromium CDP session, so this check is exercised on Chromium. If that session cannot be opened, or if it closes during the run while runtime.unhandledRejections is not "off", inspect() fails closed with a runtime.unhandled-rejection-audit-unavailable error instead of reporting a page as clean when rejections may have gone unseen. Set runtime.unhandledRejections: "off" to accept that gap and suppress the finding. Page errors and console.error are collected through portable Playwright page events and do not depend on CDP.

Collected runtime messages and stacks, page titles, and URLs are length-bounded before attachment. Markdown output collapses whitespace, replaces Unicode control characters, escapes HTML-significant characters, and escapes the punctuation that forms Markdown links and images; it does not neutralize every Markdown construct, and page content is not treated as trusted. JSON reports can still carry application data and should be reviewed before public artifact retention.

Structure

structure: {
  main: { min: 1, max: 1 },
  visibleH1: { min: 1, max: 2 },
  uniqueIds: "warning"
}

Omit max for an open upper range. { min: 0 } effectively disables a count requirement. Set uniqueIds: "off" to disable that rule.

Viewport containment

viewport: {
  horizontalOverflow: "error",
  tolerancePx: 1
}

The rule compares the document root's scrollWidth and clientWidth. Intentionally scrollable nested carousels do not fail unless they expand the document itself.

Reports

interface RenderReport {
  ok: boolean;
  url: string;
  title: string;
  viewport: { width: number; height: number } | null;
  findings: readonly RenderFinding[];
}

Each finding has a stable ruleId, severity, message, optional selector, and JSON-safe evidence. Findings are sorted by rule and selector, so repeated checks are diff-friendly. attachRenderReport() adds JSON and Markdown to the current Playwright result.

Report URLs remove embedded credentials, query strings, and fragments by default so test artifacts do not retain common token locations. URLs remain length-bounded with every policy. Opt in to full URLs only for a safe test environment, or supply a custom redactor:

await observePage(page, { urlPolicy: "full" });
await observePage(page, {
  urlPolicy: (url) => url.replace(/\/users\/[^/]+/u, "/users/:id")
});

Cleanup

Always call dispose() in finally. It removes page and protocol listeners, is idempotent, and prevents further inspection.

An observer accumulates runtime events for one page journey. Repeated inspect() calls report the same accumulated events; create a new observer for an independent navigation or retry.

Non-goals

v0.1 does not perform pixel comparison, clipping/overlap analysis, spatial alignment checks, CSS root-cause analysis, WCAG auditing, Lighthouse scoring, web-server orchestration, API mocking, cross-origin iframe inspection, or worker inspection.

Visual differences—colors, spacing, alignment, clipping or overlap, icons, and similar details—remain the domain of screenshot testing; a render contract does not inspect pixels.

Development

npm install
npx playwright install chromium
npm run verify

The browser matrix includes desktop and mobile viewports, delayed readiness, stuck busy state, three runtime failure classes, structure failures, root-level overflow, an allowed nested carousel, ignore rules, deterministic serialization, and listener disposal.

The design notes explain the lifecycle and reporting decisions behind the public API.

See CONTRIBUTING.md before adding a rule or changing report fields. Report vulnerabilities privately as described in SECURITY.md.

License

MIT