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

walkwright

v0.0.2

Published

Walkwright runtime: walk your app with Playwright and record a browsable report

Readme

walkwright

The app-agnostic half of the walk: the recorder, the reporter that describes and uploads a run, the provisioner mechanism, the account cache and the walkwright command. Nothing in here knows about any particular app — the app supplies its own config, tests and resources through the seams below.

The walk

A walk is Playwright: spec files under a walk project, and this package's reporter. walkwright walk is a thin wrapper around playwright test — there is no separate runner.

walkwright walk
walkwright walk --workers 4 --headed
walkwright walk --upload-on-fail
walkwright walk --no-upload
walkwright walk --branch main
playwright test --project=walk    # the same lanes as a smoke run: no shots

| Flag | Meaning | | ------------------- | ----------------------------------------------------- | | --upload-on-fail | upload the passing lanes' shots even though it failed | | --no-upload | skip the upload entirely | | --branch <name> | report under this branch instead of git's | | --project <name> | the Playwright project to run (default walk) | | --version | the banner | | --help, -h, ? | keys and flags | | everything else | forwarded to playwright test verbatim |

// playwright.config.ts
import { walk } from "walkwright/config";

reporter: [["list"], ...walk.reporter({ appUrl })],

projects: [{ name: "walk", use: { ...use, ...walk.use } }],

The helper reads WALK_RECORDING — set by walkwright walk, unset for a plain playwright test — and does the wiring above either way: on, it returns the reporter and recording: true; off, an empty reporter list and recording: false, so the same lanes double as smoke tests in an ordinary playwright test run with no shots taken. walk.recording is the boolean on its own, for anything else that needs to branch on it.

The reporter names the run's directory — .walkwright/reports/run-<timestamp> — before the first worker is spawned, and hands it to them in WALKWRIGHT_RUN_DIR. Every recording test shoots into that one directory.

A run always writes its directory and manifest.json locally, failed or not. A failed run uploads nothing unless --upload-on-fail — with the override, only the passing lanes' shots go up; the failing ones never made it into the manifest. --no-upload skips the upload outright, pass or fail.

| Option | Meaning | | ------------ | ------------------------------------------------------------- | | appUrl | the app the run walked, as the report viewer names it | | reportsDir | where run directories are made (default .walkwright/reports) |

See docs/run-manifest.md for the manifest's shape.

What the CLI sets for the child process:

| Variable | Set by | Meaning | | -------------------------- | ------------------ | ----------------------------------------- | | WALK_RECORDING | walkwright walk | on for every run through the bin | | WALKWRIGHT_UPLOAD_ON_FAIL | --upload-on-fail | upload a failed run's passing shots | | WALKWRIGHT_NO_UPLOAD | --no-upload | skip the upload | | WALKWRIGHT_BRANCH | --branch <name> | report under this branch instead of git's | | WALKWRIGHT_RUN_DIR | the reporter | internal — hands each worker its run dir | | WALKWRIGHT_THEME | you | light or dark; otherwise COLORFGBG |

Invoking Playwright directly instead of through walkwright walk, set the first four yourself; WALKWRIGHT_RUN_DIR is the reporter's business.

Recording

walkwright/recorder is a pair of Playwright fixtures the app extends its own test with:

import type { RecorderFixtures } from "walkwright/recorder";
import { createRecorderFixtures } from "walkwright/recorder";

const walkTest = test.extend<RecorderFixtures>(createRecorderFixtures(app));

walkTest("items", async ({ page, record, runDir }) => {
  await record(itemsPage, {}, "empty");
});

record shoots into runDir and hands the reporter what it shot when the test passes — when recording is on; off, runDir is empty and nothing is shot. A shot recorded with a page object is named after it; anything else is attributed by the URL, matched against the pages definePage registered, and ShotOptions (category, page, state) overrides any part of that.

runDir is also where a test writes artifacts that belong beside its shots.

Logging in

walkwright login [--force] [--url <server>] sets a project's upload token up on its own — the same device-authorization login and implicit project creation the reporter's first upload needs. It writes .walkwright/config.json; without one, a green run writes its manifest and skips the upload. --url picks the reports server (default https://app.walkwright.dev) and is written into the config next to the project id and token, so the reporter uploads to the same server the login happened on. --force --url repoints a project.

WALKWRIGHT_BRANCH names the branch a run is reported under when git cannot (a detached CI checkout).

Files

Everything walkwright writes into a project lives under one folder:

.walkwright/
  config.json          walkwright login — project id, token, server; commit it
  reports/run-<ts>/    the reporter — shots and manifest.json
  store/worker-<n>/    walkwright/store — worker-scoped caches
  store/shared/        walkwright/store — project-scoped caches
  .gitignore           written by walkwright — ignores all but config.json

The project is the package the walk runs from: the nearest ancestor of the cwd with a package.json (or an existing .walkwright/), never the git toplevel — in a monorepo the e2e package is its own project. The folder's .gitignore keeps everything but the config out of git, and is itself ignored, so nothing under .walkwright/ but the config ever shows up in a status. walkwright only writes that file when it is missing: a project that must not commit its token (a public repo) keeps a local *-only one.

Stores

walkwright/store keeps a JSON value across runs so the app never touches the file system itself:

import { createStore } from "walkwright/store";

const accounts = createStore("account", { schema: accountSchema });

const account = accounts.read() ?? (await signUp());
accounts.persist(account);

| Option | Meaning | | -------- | ------------------------------------------------------------------------------------------- | | scope | worker (default) — one file per parallel worker; project — one file for all | | schema | a zod schema: read decodes through it and ignores a file that does not, persist encodes |

A worker-scoped store lives under store/worker-<n>/, keyed by Playwright's TEST_PARALLEL_INDEX; a project-scoped one under store/shared/. Writes go through a temporary file and a rename, so a killed worker never leaves half a file, and a project-scoped store that several workers save at once ends up with whichever finished last, intact. A file that is missing, not JSON or fails its schema reads as undefined and is reported, never thrown: the caller makes a fresh value instead. clear removes the file; path is where it lives.

walkwright/storage-state is the same for a browser session:

import {
  createStorageStateStore,
  storageStateOf,
} from "walkwright/storage-state";

const session = createStorageStateStore("session");
const services = createStorageStateStore("services", { scope: "project" });
const github = createStorageStateStore("github", {
  scope: "project",
  cookies: (cookie) => cookie.domain.endsWith("github.com"),
});

test.extend({ storageState: storageStateOf(session, services) });

await session.save(page.context());
await github.restore(page.context());

save writes the context's storage state; with a cookies filter it keeps the matching cookies only and no origins. restore adds the saved cookies to a context that already exists, for a session picked up part-way through a test. storageStateOf is for Playwright's storageState option: the path of the first store that has a file, or emptyStorageState when none does.

Modules

| Import | Role | | ------------------------- | ---------------------------------------------------------- | | walkwright/reporter | the run directory, manifest.json, the upload | | walkwright/recorder | the recorder, and the fixtures that bind it to a test | | walkwright/pom | definePage: page objects, dialogs, fragments | | walkwright/browser | hideStyle: an init script hiding chrome that would move | | walkwright/provisioner | resources created through the app, disposed in reverse | | walkwright/store | a JSON value cached across runs, per worker or per project | | walkwright/storage-state | a browser session cached across runs, for storageState |

Page objects

walkwright/pom has one grammar for every definer:

defineX(definition).withActions(map); // a literal
defineX((...params) => definition).withActions(map); // a builder, params-first

| Definer | Definition | Instance | | ----------------- | ----------------------------------------------------- | ---------------------------------------------- | | defineComponent | { root?, elements? } — root present: rooted | { root?, ...elements, ...actions } | | defineDialog | { name, role?, trigger?, dismiss?, elements? } | { root, open, close, waitFor, ... } | | definePage | { path, name, header?, fragments?, elements?, ... } | { goto, url, waitFor, elements, ...actions } | | defineLayout | { path, group?, chrome?, parent? } — literal only | shared by its pages |

Element entries take the context first, own params after; the annotation is inferred:

| Entry | Instantiates to | | ----------------------------------------------- | ---------------------------------- | | back: link("Back") | a value — a leaf, called | | deployment: link | a method — a leaf, mounted bare | | email: ({ scope }) => scope.getByLabel(...) | a value — ≤ 1 parameter | | member: ({ scope }, email: string) => ... | a method — ≥ 2 parameters | | first(): Locator { return this.rows.first() } | a value — this is the siblings | | row: defineComponent((name) => ...) | a method — a builder, mounted bare |

The builder form comes back branded (parameterized: true): mounted under a key it is a method, elements.row("a"), and its params reach the actions context as params. parameterized() brands hand-written leaves the same way; nothing else needs it.

defineComponent, defineOverlay, defineDialog, definePage, page presets and defineApp all take a capture: { rules } option — declarative rules that rewrite dynamic text (counters, relative times, ids, user names) in the live DOM right before a screenshot and restore it right after, so walks stay stable. See docs/descriptor-schema.md#capture-rules.

Actions (withActions) receive the context — scope, page, elements, plus root for rooted components and dialogs, params for builders — then their own params, and see sibling actions as this. Actions never mint locators: locating is for elements and root. A page's actions orchestrate its elements, including those its fragments spread in; a fragment's actions are for callers.

Every withActions member and every top-level factory names its return type (explicit-function-return-type); nothing else needs an annotation — a ReturnType<typeof page> alias, or an explicit interface for a page that navigates in a cycle with another, is the exception.

Seams

| Seam | The app supplies | | ------------------------------------------ | ----------------------------------------------------------------- | | the walk project | the walks themselves, and the session they run as | | ["walkwright/reporter", { appUrl }] | the app the report is of | | test.extend(createRecorderFixtures(app)) | the test the recorder shoots through, and the page inventory | | definePage({ afterGoto }) | the readiness a page object's goto waits out | | createProvisioner(page, kinds) | the resource kinds, how each is made, and how it is disposed | | createStore(name, { scope, schema }) | the value cached across runs, its shape, and how far it is shared |

Building

Consumed as a built package — the reporter and the walkwright bin both run under plain node, which resolves walkwright through node_modules to build/. The bin is bin/walkwright.js, a checked-in launcher for build/cli.js, so it links whether or not the package has been built yet. After changing anything here:

pnpm build

Consumers install it from the filesystem ("walkwright": "link:…/packages/sdk") and build it before they run, so a fresh build is automatic.