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

@specific.dev/spectest

v0.138.2

Published

Spectest SDK for defining test environments in TypeScript.

Readme

Spectest SDK

Spectest runs your application and its dependencies in isolated environments. Tests can fork a parent's live state, including memory and files, then drive user flows with recorded assertions and graphical replays.

Install the SDK in your project's spectest/ directory:

mkdir -p spectest
npm install --prefix spectest --save-exact @specific.dev/spectest

Install the Spectest CLI and run spectest docs /installation for project setup and authentication. spectest docs contains the complete offline authoring guide.

For browser tests, read spectest docs /tests/browser; for phone-emulated Expo apps, read spectest docs /tests/mobile. The shared test model is in /tests. Prefer unique, scoped role/name or test-ID locators and awaited browser assertions: await expect(browser.getByRole("status")).toHaveText("Saved"). A one-time read inside expect does not retry. Observe the exact saved state before dependent work; do not replace readiness with sleeps or arbitrary .first() selections. SDK 0.123.0 uses native Playwright assertions while retaining Spectest's API and recording. Ambiguous single-element selectors fail immediately; make them unique.

For terminal readiness, use terminal.waitFor directly. It retries the condition and throws on timeout, so neither an outer ctx.wait nor an extra assertion is needed to check that text appears:

await terminal.waitFor("Rack is initialized", /Current rack status:\s*Initialized/, {
  timeoutMs: 35 * 60_000,
  intervalMs: 5_000,
  failOn: [
    { description: "Setup was rejected", matcher: "Start Rack Setup Failed" },
    { description: "Initialization failed", matcher: "Initialization Failed" },
    { description: "Initialization panicked", matcher: "Initialization Panicked" },
  ],
});
await terminal.press("Ctrl+C"); // Separate cleanup after the condition succeeds.

SDK 0.129.0 records the string/regex matcher (or identifies a custom predicate), timeout, and start/end positions for this wait. The dashboard shows its condition and outcome and replays up to the result. Later inputs belong to their own steps; the full terminal session can be opened separately. ctx.wait remains useful when a retry must repeat actions, rather than just observe terminal state.

Terminal waits record matched, timed-out, failure-condition, predicate-error, process-exit and cancelled outcomes. failOn checks the rendered screen before checking success; string and regex conditions are recorded explicitly. For a custom predicate, provide condition: "Expected sled has a valid bootstrap IPv6 address" to explain the actual check separately from its step title. Async predicates are awaited, and their rejection is recorded. A final matching screen succeeds even if the process has exited; otherwise exit fails the wait after final output is drained. No Promise.race with terminal.exited is needed.

Pass signal: controller.signal to cancel a wait without closing its terminal. Cancellation stops SDK polling; it cannot forcibly stop work started inside a custom predicate. terminal.close({ timeoutMs: 10_000 }) cancels pending waits, sends EOF, escalates to a kill, and bounds cleanup including output drain. The default close deadline is 10 seconds; repeated calls share the same result or failure. terminal.exited records the exit when observed after completion, not when its promise is first read. A stalled output drain fails after 5 seconds instead of hanging indefinitely. Test code should preserve its original failure if diagnostic or cleanup operations also fail.

Browser navigation accepts public Playwright-style options, without a backend cast or pageOp call:

await browser.goto(consoleUrl, { timeout: 120_000, waitUntil: "domcontentloaded" });
await browser.reload({ timeout: 120_000 });

These options also apply to goBack and goForward. Navigation deadlines do not cancel a surrounding ctx.poll, and retrying a navigation after a visible error is a recovery action that should remain visible in the test results.

Pub/Sub integrations are project-owned as of SDK 0.98.0. The former pubsub/pubsubClient exports and /components/gcp entry point are removed. Use a local service definition or serviceGroup() wrapper; the repository’s examples/google-pubsub demonstrates this using public SDK APIs.

k3s registry images are pulled when the environment starts (0.138.2)

The registry strings in k3s({ images }) are now pulled when bootstrap starts, alongside service images and the environment's images, instead of after the cluster is running. Setup loads them into every node from that local copy, so services that depend on the cluster start sooner. Each pull gets up to three attempts; an image that cannot be pulled at the start is pulled by the nodes during setup, also with up to three attempts. References to the cluster's own registry are still pulled once that registry is up.

Image pulls retry network failures (0.138.1)

A service image pull that fails at the network level (DNS, refused or reset connections, TLS handshake timeouts, or an upstream 5xx) is retried twice, after 2 and 5 seconds, before environment setup fails. A missing image, denied access, a registry rate limit or the pull's own timeout still fails at once.

Browser coverage through navigation away from the app (0.137.1)

A click that navigates from a covered app to another site (an OAuth or GitHub installation page, for example) no longer fails coverage capture with "Browser script identity changed". Chromium delivers a coverage take sent during that navigation to the incoming page; the SDK now attributes it to the renderer whose scripts it reports, and uses sources it retained before the old page went away. Genuinely ambiguous takes still report incomplete coverage.

Browser coverage across navigation (0.131.17)

Browser coverage keeps script identities separate when Chromium changes renderers during cross-site navigation, including login redirects. Anonymous browser code cannot inherit an earlier script’s URL or source map. No application configuration change is needed. A script identity change during capture is reported as incomplete coverage instead of publishing uncertain attribution.

Next.js coverage (0.131.16)

Use coverage.next({ appDir: "ops", sourceRoot: "." }) in your Spectest service configuration to collect server and browser coverage. Keep Next's server and browser source maps in the runtime image. appDir is relative to the container working directory; sourceRoot identifies the repository directory represented by the build's source-map root.

Standalone servers launched through Node wrappers can change working directory: the adapter preserves the launcher's generated dependency identity in children. Turbopack's generated image modules, including SVG data-URL wrappers, are excluded automatically wherever the asset lives. Authored asset and application sources are not excluded.

For Next 16.4.3, the adapter also corrects a reproduced Turbopack Edge source-map path defect using Next's resolved build manifest (appDir, distDir, and turbopack.root). This happens while the collector reads a map, before it creates the coverage record. No application build-script workaround is required. The correction is scoped to that verified version and layout; correctly emitted maps and other versions retain normal source-map interpretation. Unknown source paths remain unresolved and cannot authorize subset selection.

Supabase function coverage (0.122.0)

supabase({ dir: "supabase" }) automatically collects coverage for its edge functions, including local shared modules outside that directory. No application instrumentation or coverage option is needed. Custom component names and folder layouts work the same way. Stacks without functions have no extra collector.

Coverage starts before user code in both shared and per-function isolates. Setup and individual test captures are separate, with source maps checked against the uploaded TypeScript/JavaScript. Generated startup gates let functions warm concurrently while preserving their Deno configuration and import maps. Isolate captures, source metadata reads, and report writes run concurrently. Lost workers or unusable source metadata fail collection rather than silently reporting no execution. Changes to the function catalogue or runtime configuration conservatively invalidate subset selection, including newly added functions. This collects edge function code only: SQL and application frontend coverage are separate, and subset enforcement remains a project setting.

Interactive entrypoints (0.114.0)

Choose the links shown in the interactive dashboard and PR preview comment:

const env = defineEnvironment({
  name: "my-app",
  services: { web, api },
  interactive: {
    instructions: "Sign in as [email protected] and use 000000 as the code",
    entrypoints: [
      { name: "App", url: "https://app.test" },
      { name: "Admin", url: "https://app.test/admin" },
      { name: "API shell", terminal: "api" },
    ],
  },
});

List order is preserved. URLs must match declared service hostnames and are translated to public preview URLs, retaining paths, queries and fragments. Terminal targets are typed service names and use dashboard member permissions. Entrypoints are display metadata; they do not change exposure or test execution. With no entries, the only preview link is Open environment. Optional interactive.instructions is a concise, direct plain-text sign-in description, shown in a collapsed Instructions section in the dashboard and PR comment. Blank instructions are omitted.

Live APIs in interactive environments

Add live to a scripted fake to forward its entire HTTP and WebSocket surface in interactive environments, without recording requests or responses:

const soniox = defineFake({
  name: "soniox",
  hostnames: ["api.eu.soniox.com", "stt-rt.eu.soniox.com"],
  state: createState,
  helpers: createHelpers,
  handler: handleHttp,
  websocket: handleWebSocket,
  live: {
    inject: [{
      headers: { authorization: "Bearer {{secret:SONIOX_API_KEY}}" },
    }],
  },
});

Tests and shared baseline setup always use the scripted handlers, even when credentials are available. Interactive forwarding starts after the shared baseline and before interactive seeding. Every declared hostname forwards to that same real host over HTTPS/WSS on port 443. Scripted handlers are bypassed; helpers remain available but do not observe or control live provider traffic.

live: {} forwards without credential overrides. live.inject uses the same first-matching header rules as replayFake for HTTP requests and WebSocket handshakes; matches select credentials, not which traffic is forwarded. live.sign supports the same AWS SigV4 signing for HTTP (not WebSockets). Secret references are resolved from the project's Secrets store. Missing credentials and upstream failures surface as errors without scripted fallback.

For Soniox, use header authentication and omit api_key from the initial WebSocket message. The application can use a dummy key; Spectest overwrites its outbound Authorization header with the real one. Provider callbacks and provider-fetched audio URLs must separately be reachable in the environment.

Unlike replayFake, live fakes have no cassette state or recording helpers. HTTP payloads may be buffered during forwarding and WebSocket opening buffers are bounded; neither is retained as a recording. Application-level storage and logs remain under application control.

Microphone fixtures and WebSocket fakes (0.103.0)

Supply a 16-bit PCM mono/stereo WAV when creating a browser session:

const browser = await ctx.browser({
  microphone: { path: "spectest/tests/fixtures/speech.wav" },
});

The microphone uses Chromium's native capture pipeline. It plays once and then supplies silence; add loop: true to repeat. Paths are relative to the repository root. Set the fixture on the first call for that browser session; reattaching preserves its source. Fixture sessions use a dedicated Chromium process to keep named browsers' audio isolated.

A fake can serve HTTP and WebSockets on the same hostname, sharing state and helpers:

const speech = defineFake({
  name: "speech",
  hostnames: ["speech.example.com"],
  state: () => ({ messages: [] as string[] }),
  handler: () => Response.json({ ready: true }),
  websocket(req, state) {
    if (new URL(req.url).pathname !== "/realtime") {
      return new Response("Not found", { status: 404 });
    }
    return {
      message(socket, message) {
        if (typeof message === "string") state.messages.push(message);
        socket.send(message);
      },
    };
  },
  helpers: ({ state }) => ({ messages: () => [...state.messages] }),
});

The handshake returns a rejection Response or callbacks (open, message, close), with an optional selected protocol. Callbacks may be async and run in order per connection. WS and trusted WSS support text/binary messages; observe traffic through the fake's helpers.

Chat reactions (0.105.0)

A chat annotation's message can carry the tapbacks a messaging app puts on a bubble. Each reaction is { emoji, from?, new? }: from is self (the end user) or other (the default), and new marks one that arrived in this step, exactly like a message's new. A removed reaction simply stops being listed.

return annotate(turns, "chat", {
  messages: turns.map((t) => ({
    side: t.fromCustomer ? "self" : "other",
    text: t.body,
    reactions: t.acked ? [{ emoji: "✅", new: t.ackedNow }] : [],
  })),
});

The dashboard draws the chips on the bubble's lower corner, opposite the sender, and slides a new one in after the bubbles that arrived with it.

Read receipts and typing (0.106.0)

A chat message can carry its delivery status (delivered or read) and an optional statusAt — free text such as "23:14" — drawn as the caption a messaging app shows under the bubble, on the bubble's own side. The annotation itself takes typing: "self" | "other", drawn as the three-dot bubble at the end of the thread on that side. Both are the fake's to set, like new; typing is transient, so a step either shows it or not.

return annotate(turns, "chat", {
  typing: state.agentTyping ? "other" : undefined,
  messages: turns.map((t) => ({
    side: t.fromCustomer ? "self" : "other",
    text: t.body,
    status: t.readAt ? "read" : "delivered",
    statusAt: t.readAt,
  })),
});

Inline replies (0.107.0)

A chat message can carry an id — any non-empty string the fake chooses, the provider's message handle usually; it is never drawn — and a reply names an earlier message's id as replyTo. The dashboard draws the reply the way a messaging app threads one: an excerpt of the earlier message in a muted mini-bubble directly above the reply, on the reply's side, with a short connector between them. A reply to the bubble right above it draws only the connector, since the message it answers is already on screen. A run of consecutive replies to one message quotes it once, above the first of them, and the rest draw only the connector so they read as one thread; a reply to a different message, or an ordinary message, ends the run. A replyTo that names no earlier message of the same call draws nothing extra; the bubble is an ordinary message.

return annotate(turns, "chat", {
  messages: turns.map((t) => ({
    id: t.id,
    side: t.fromCustomer ? "self" : "other",
    text: t.body,
    replyTo: t.inReplyTo,
  })),
});

HTTP assertions

ctx.fetch() returns a wrapped response. Status fields, header lookups, and all body readers retain the HTTP call's provenance, so assertions appear under that call on the timeline:

const response = await ctx.fetch("http://app:3000/image.png");
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("image/png");
const bytes = await response.bytes();
expect(bytes).toHaveLength(70);
expect(bytes[0]).toBe(0x89);
expect(bytes.slice(0, 8)).toEqual(new Uint8Array([0x89, 0x50, 0x4e, 0x47, 13, 10, 26, 10]));

arrayBuffer() wraps buffer metadata and slices. blob() wraps metadata and its body readers. formData() wraps get(), getAll(), and has(), including uploaded files and their metadata. Headers also support wrapped getSetCookie() and Bun's getAll(), toJSON(), and count. Missing fields remain null, with provenance recovered by an immediately following expect.

Use .unwrap() to pass a wrapped value to a native API, such as new TextDecoder().decode(bytes.unwrap()). Use .transform("text", bytes => new TextDecoder().decode(bytes)) when the derived value should retain provenance. Iteration and callback arguments remain raw, as do array/typed-array length and stream control flags (done, locked). Use expect(bytes).toHaveLength(n) for a linked length assertion. bodyUsed is wrapped: unwrap it for control flow.

For an open-ended stream, pass { captureBody: false } to return without waiting for recorder body capture. SSE (text/event-stream) skips capture automatically. The HTTP event and assertions are still recorded; the event omits the body. response.body.getReader().read() returns a raw done flag and wrapped value. BYOB readers, tee(), and pipeThrough() preserve provenance too. Release reader locks and cancel streams when finished, as with native fetch.

Electron apps

Use electron() to build and run a real Linux Electron app under Xvfb, with its main process, preload, IPC and renderer. Spectest supplies the container setup; no custom Dockerfile is needed.

Create spectest/index.ts:

import { defineEnvironment } from "@specific.dev/spectest";
import { electron } from "@specific.dev/spectest/components";

export const env = defineEnvironment({
  name: "desktop-app",
  services: { app: electron() },
});

export default env.project();

Create spectest/tests/notes.ts, using your app's labels:

import { expect } from "@specific.dev/spectest";
import { env } from "../index";

env.test("Save a note", async (ctx) => {
  const app = await ctx.desktop(ctx.svc.app);
  await app.getByRole("textbox", { name: "New note" }).fill("Hello desktop");
  await app.getByRole("button", { name: "Add note", exact: true }).click();
  await expect(app.getByRole("listitem")).toHaveText(["Hello desktop"]);
});

Run spectest test . from the project root. The dashboard records desktop interactions and replays the app with window chrome derived from its Electron configuration. Child tests using dependsOn inherit the live app state, including unsaved input, in their own isolated environment.

The app must declare Electron in package.json (its own or, with context set to a monorepo root, the root one). Defaults are npm ci with a package lock (otherwise npm install), npm run build, and the package's main entry. Use appDir, installCommand, buildCommand, entry, and nodeVersion to customize the build. env supplies runtime variables; dependsOn waits for backend services in the same environment. Expose the backend through the built-in proxy (tls: [{ hostname, port }]) and give the app its https:// URL: a renderer Content Security Policy usually refuses a plain http:// service address.

Applications need Linux-compatible dependencies and binaries. Native OS dialogs and macOS-specific APIs are outside this basic support. Replays capture the renderer DOM from acquisition onward, not native OS surfaces.

Run spectest docs /components/electron for the full configuration reference, backend setup, session behavior, and replay scope (CLI 0.2.124 or later).

k3s({ registry: { host: "registry.test" }, images: [...] }) exposes the component's writable registry over verified HTTPS through the VM proxy. Dockerfile builds in images targeting registry.test/... are published and preloaded from the same cached build, preserving digests without an export/push step. Other images keep direct preloading. Registry writes stay isolated across test forks; client and containerd CA trust are configured automatically.

HTTP/2 and gRPC ingress

For a service listening on plaintext HTTP/2 (h2c), declare tls: [{ hostname: "grpc.example.test", port: 50051, http2: true }]. Clients use trusted TLS on port 443. The ingress selects HTTP/2 with ALPN, routes each stream by :authority, and preserves streaming bodies, metadata, trailers, and cancellation. HTTP/1.1 requests still use the existing HTTP proxy. Runtime services support the same option. HTTP/2 streams currently bypass ctx.intercept and HTTP request recording.

CORS at ingress

Fake and service ingress default to cors: "passthrough": OPTIONS requests reach the handler or upstream through ctx.intercept, and response CORS headers are preserved, including their absence on rejections or responses with no CORS policy. Allowed credentialed requests depend on the application's own headers.

Automatic CORS is opt-in per fake or service hostname:

defineFake({ name: "api", hostnames: ["api.test"], cors: "permissive", handler });
// On a boot or runtime service:
tls: [{ hostname: "api.test", port: 3000, cors: "permissive" }]
// On a Supabase gateway (including responses from interceptors):
supabase({ hostname: "supabase.example.com", cors: "permissive" });
// On a low-level component proxy:
proxy("api.test", { service: "api", port: 3000, cors: "permissive" });

Permissive HTTP/1 ingress answers CORS preflights before interceptors and adds reflected allow-origin plus allow-credentials to responses without allow-origin, including errors. Ordinary OPTIONS still reaches the handler. Use passthrough when testing application CORS policies. HTTP/2 streams always pass CORS through.

Prefer the application's production hostnames for the frontend and API inside the environment: Spectest resolves declared names to its test services and provides certificates trusted inside the environment. This preserves existing origin allowlists and cookie relationships. Different subdomains are still different origins; a synthetic response must still supply the intended CORS headers. Permissive mode is a convenience override and should not be used to verify application CORS restrictions.

Annotated service helpers (0.108.0)

Service helpers support the same return annotate(value, "email" | "chat", options?) contract as fake helpers. The SDK opens the annotation box, records one named service call, and returns a wrapped value whose assertions link to that call. The annotated call's internal operations are suppressed; concurrent work is unaffected. Ordinary helper returns retain their values and underlying events.

helpers: ({ fetch }) => ({
  async latest() {
    const response = await fetch("http://mail:8080/messages/latest");
    const mail = (await response.json<{ subject: string; text: string }>()).unwrap();
    return annotate(mail, "email");
  },
})

The classic viewer shows mail.latest() with Email/JSON tabs; JSON contains helper arguments and the returned value. The SPA retains its nested presentation. annotate always returns a box for the helper boundary; it does not attach views to arbitrary recorded results. See spectest docs /services for a complete example.

SIP phone calls

const phone = await ctx.phone({
  endpoint: "sip:5060",
  from: "sip:[email protected]",
  language: "sv",
});
await phone.call("receptionist");
await phone.say("Hej! Jag vill boka en tid.");
await phone.hangup();

The phone offers all installed codecs, preferring Opus then higher sample rates, with G.711 and other legacy codecs available as fallbacks. Recordings use 48 kHz mono PCM for capture and continuously encode it as 48 kbps VBR Opus/WebM. Encoding runs off the SIP callback thread and writes no intermediate WAV; the original audio timeline and waveform data are preserved.

Supertonic 3 runs locally using models installed in the golden image. Each call, say, and hangup is recorded. One continuous recording captures both sides from dialing through hangup, including replies and pauses between steps. Each say clip starts when speech is transmitted, skipping generation time, and ends when that step completes. Both call(destination, { waitFor }) and say(text, { waitFor }) poll application state, then wait for incoming silence. For call, polling starts after the call connects, allowing the greeting to finish within the call step. For say, polling starts after outgoing playback. Without waitFor, call ends when connected and say ends after outgoing playback. The wait has no timeout of its own: the test case's timeoutMs is the only ceiling, and a bound on the reply belongs inside the predicate (for example a ctx.poll with its own timeoutMs), where it appears in the timeline. The quiet period (700 ms) and poll interval (100 ms) are internal defaults for now. waitFor may return a nonempty transcript string instead of true; after the silence wait, it appears as Response below Caller in an indented chat row beneath the waveform. Return false or "" to keep waiting. Your test supplies the text from application state or a fake; an async callback can wait and then return it. Playback starts muted and remembers your mute choice across steps, with overlapping blue caller and violet recipient waveforms and replay controls. Both sides play together from the same recording. Step playback trims silence at the edges of utterances and waited greetings; greetings also show the approximate time to first audio. Hangup steps have no player. The complete recording remains stored. Outgoing text remains a plain transcript; incoming audio is not transcribed automatically. Calls close and recordings finalize automatically at case end. This initial API requires SDK 0.112.0 and a golden image with phone support.