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

visual-storyboard

v0.3.6

Published

Core types, transport interfaces, writer helpers, and default transports for visual-storyboard.

Readme

visual-storyboard

A toolkit for capturing named screenshots during automated tests and viewing them as a visual storyboard. Works with any testing tool; ships with a built-in Playwright integration.

Example storyboard

Click the image to open the live example in the viewer.

Source and issue tracker: github.com/dtinth/visual-storyboard


Playwright integration

The quickest way to get started. Create a support file:

// e2e/support.ts
import { test } from "@playwright/test";
import { PlaywrightStoryboard } from "visual-storyboard/integrations/playwright";

export const storyboard = new PlaywrightStoryboard({ test }).install();

By default storyboards are written to test-storyboards/<slug>/storyboard.ndjson next to test-results/ (add test-storyboards/ to .gitignore). To write elsewhere, pass a custom transport factory:

import {
  PlaywrightStoryboard,
  createPlaywrightFileOutputTransportFactory,
} from "visual-storyboard/integrations/playwright";

export const storyboard = new PlaywrightStoryboard({
  test,
  transport: createPlaywrightFileOutputTransportFactory("my-storyboards"),
}).install();

Then call capture in your tests:

// e2e/my.spec.ts
import { test } from "@playwright/test";
import { storyboard } from "./support";

test("checkout flow", async ({ page }) => {
  await page.goto("/");
  await storyboard.capture("Login page", page.getByRole("button", { name: "Login" }));
  // ... interact ...
  await storyboard.capture("Order confirmation", page.locator(".confirmation"));
});

capture accepts a locator (records a bounding-box highlight and scrolls the element into view) or a page (full-page screenshot, no highlight). An ARIA snapshot is always captured alongside the screenshot.

After each test the integration automatically captures a final "End of test" frame and closes the transport.

When to capture

There are two natural moments to call capture:

1. Before performing an important action — capture the UI in the state that prompted the action. For example, before clicking a button:

const submitButton = page.getByRole("button", { name: "Submit" });
await expect(submitButton).toBeVisible();
await storyboard.capture("Ready to submit", submitButton);
await submitButton.click();

If you provide a beforeCapture hook that already waits for the element to be stable and visible, the explicit assertion can be skipped.

2. After an action, once the outcome is verified — assert the expected result first, then capture. This ensures the frame shows a confirmed, stable state rather than a transitional one:

await submitButton.click();
await expect(page.getByRole("heading", { name: "Order confirmed" })).toBeVisible();
await storyboard.capture("Order confirmed", page.getByRole("heading", { name: "Order confirmed" }));

Tip: extract locators into consts. A locator often appears in the assertion, the capture call, and the action itself. Extracting it avoids repetition:

const confirmationHeading = page.getByRole("heading", { name: "Order confirmed" });
await expect(confirmationHeading).toBeVisible();
await storyboard.capture("Order confirmed", confirmationHeading);

PlaywrightStoryboardOptions

| Option | Type | Description | | --------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | test | TestLike | The Playwright test object (required) | | transport | StoryboardOutputTransport \| PlaywrightOutputTransportFactory | Transport instance or per-test factory. Defaults to defaultPlaywrightOutputTransportFactory (test-storyboards/ dir) | | enabled | () => boolean | Return false to disable capture (e.g. based on an env var) | | beforeCapture | (locator) => Promise<void> | Hook called before each locator capture — use it to wait for animations to settle |


Core API (visual-storyboard)

StoryboardWriter

High-level helper that manages slug uniqueness and delegates I/O to a transport.

import { StoryboardWriter } from "visual-storyboard";
import { FileTransport } from "visual-storyboard/transports/file";

const writer = new StoryboardWriter({
  storyboardId: "my-test",
  transport: new FileTransport({ outputDir: "out/my-storyboard" }),
});

await writer.writeInfo({ title: "My test", annotations: { branch: "main" } });
await writer.createFrame("Step 1", { imageBuffer, highlights: [], viewport });
await writer.finalize();

writer.writeInfo(options) — writes an info event with a title, optional description, and arbitrary key-value annotations.

writer.createFrame(name, options) — uploads the screenshot asset and appends a frame event. Options: imageBuffer, highlights, viewport, annotations (e.g. { ariaSnapshot }), imageContentType.

writer.finalize() — flushes and closes the transport.

FileTransport

Writes everything into a single directory: storyboard.ndjson plus screenshot assets. Asset URLs in the NDJSON are relative, so the directory is fully self-contained and portable.

import { FileTransport } from "visual-storyboard/transports/file";

new FileTransport({ outputDir: "out/my-storyboard" });
// out/my-storyboard/storyboard.ndjson
// out/my-storyboard/<frame-slug>.png

Custom transports

Implement StoryboardOutputTransport to send storyboards anywhere:

import type { StoryboardOutputTransport } from "visual-storyboard";

class MyTransport implements StoryboardOutputTransport {
  async writeAsset(asset) {
    // upload asset.data (Uint8Array), return { url, contentType, byteLength, sha256 }
  }
  async writeEvent(event) {
    // append JSON line to your storage
  }
  async close() {
    // flush / release resources
  }
}

NDJSON format

Each line is a JSON object with a type field:

  • { type: "info", version: 1, time, title, description?, annotations? } — storyboard metadata, written once at the start
  • { type: "frame", version: 1, time, name, slug, screenshot, highlights, viewport, annotations } — one per captured step; screenshot.url is relative to the NDJSON file