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

@junoflow/testkit

v0.3.0

Published

Test harness for Juno integrations

Readme

@junoflow/testkit

Test harness for Juno integrations. It runs your handlers with the same ctx the sandbox builds, but with a different implementation for tests.

npm install --save-dev @junoflow/testkit
import { harness } from '@junoflow/testkit';
import manifest from '../manifest.json';
import integration from '../src/integration';

const h = harness({ manifest, integration });

// Mocked routes, the store, findings, events and logs otherwise outlive an
// `it`, and the test that fails is whichever one ran second.
beforeEach(() => h.reset());

test('sendNotification posts to the API and returns the receipt', async () => {
  h.credentials({ appToken: 'tok', userKey: 'usr' });
  h.http.post('https://api.pushover.net/1/messages.json', {
    json: { status: 1, request: 'r1' },
  });

  expect(await h.action('sendNotification', { message: 'hi' })).toEqual({
    request: 'r1',
  });
  // Nothing standing: the success path proved the credentials work.
  expect(h.findings()).toEqual([]);
});

test('conforms to its manifest', () => {
  expect(h.conformance()).toEqual([]);
});

Note the plain import manifest from '../manifest.json' — the shared tsconfig.base.json compiles as CommonJS, where an import attribute (with { type: 'json' }) is a TS2823 error. resolveJsonModule is on, so the plain form works.

API

harness({ manifest, integration }) returns:

| Member | Does | | -------------------------------------- | ------------------------------------------------------------------------ | | h.config(values) | set non-credential slots (manifest defaults are already applied) | | h.credentials(values) | set credential slots | | h.action(name, params) | run an action, returns its result | | h.webhook(name, request?) | run a webhook; request defaults to POST / with an empty body | | h.schedule(name, event?) | run a schedule handler | | h.hook(name) | run a declared lifecycle hook (e.g. activated) | | h.listen({ until, events, timeout }) | run the listener until the condition holds, then abort | | h.store | the KV, as a plain Map (get/set/delete all land here) | | h.findings() | what ctx.findings asserted, as {slug, key, severity, text, standing} | | h.states() | values ctx.states.publish has sent, keyed by local name | | h.status() | the last ctx.status.set report, or null if it never reported | | h.emitted() | [{ event, payload }], in order | | h.logs() | [{ level, message, data }] | | h.conformance() | manifest-vs-module problems; [] when they agree | | h.http | the request mock, below | | h.reset() | clear recorded state, keeping config and credentials |

h.http

Replaces global fetch for the duration of a handler and restores it after, including when the handler throws.

h.http.post('https://api.example.com/send', { receipt: 'r1' }); // 200 JSON
h.http.get(/\/status$/, { status: 503, json: { down: true } }); // RegExp matching
h.http.post('https://api.example.com/send', (req) => ({ echoed: req.body }));

h.http.requests; // every request a handler made, in order

An unmatched request throws, to ensure that all HTTP calls are expected.

A canned answer is either the upstream's JSON body verbatim, or the envelope { status?, headers?, json?, body? }. The two are told apart by an exclusive key match: an object is an envelope only when every key it has is one of those four. So an upstream payload that happens to contain status — Pushover's { status: 1, request: '…' } — is a body, as written. When a payload consists only of envelope keys, say so: { json: { status: 1 } }.

h.listen

await h.listen({ events: 3 }); // until three events are emitted
await h.listen({ until: (h) => h.store.get('cursor') === '42' });

After the condition holds, ctx.signal aborts and the harness waits for the listener to return. A listener that ignores its signal fails the test — in production it hangs the supervisor on shutdown instead.

h.conformance()

Checks the manifest and the module against each other: every declared surface implemented (actions, webhooks, schedules, hooks), nothing undeclared exported, a listener function exactly when the manifest declares a listener section, every hook name inside Juno's closed vocabulary, and every schema compiling. states is a manifest declaration, not a handler surface, so it plays no part here — see h.states() instead. Returns all problems rather than the first — "you declared six actions and implemented four" is one fix, not four runs.

h.findings()

expect(h.findings()).toEqual([
  {
    slug: 'badCredentials',
    key: '',
    severity: 'error',
    text: 'Pushover rejected the credentials',
    standing: true,
  },
]);

open, clear and set all land here, each returning the transition it made ({ opened, cleared }, both false when it refreshed or retracted nothing). An undeclared name fails the test, naming the declared ones — Juno rejects it at runtime, and hearing that from an installer is not a test. An instance the handler retracted stays in the list with standing: false; one that was never open records nothing at all, so expect(h.findings()).toEqual([]) is the success-path assertion for a handler written with set().

h.states()

await h.schedule('refresh');
expect(h.states()).toEqual({ workday: true, 'day-kind': 'workday' });

ctx.states.publish(values) is on the base context, so an action, webhook, schedule or hook can all call it. h.states() is the current picture — a second publish of the same name overwrites, it does not accumulate — keyed by the local name the handler used, not the <install>:<name> id Juno scopes it to on the wire. An undeclared name fails the test, the same way an undeclared finding or event does.

h.status()

await h.schedule('fetchForecast');
expect(h.status()).toEqual({
  summary: '2.4 °C, cloudy — 96 intervals to 2026-09-27',
  details: { intervals: 96 },
});

ctx.status.set is on the base context, like ctx.states.publish. The report is replaced on every write, so h.status() is the last one sent, and null when the handler never reported. That is a result worth asserting too: an integration with nothing to say should leave the status absent. details is left out when the report had none or had an empty map. A report Juno would reject fails the test: an empty or multi-line summary, a summary longer than 200 characters, or a detail that is not a string, number or boolean. The error names the key.

h.hook(name)

await h.hook('activated');

Runs a declared lifecycle hook with the same context a schedule gets. An undeclared hook, or one whose handler returns a value, fails the test — "a hook reports through ctx and returns nothing" is what production says too. h.conformance() also rejects a hook name outside Juno's closed vocabulary (today, only activated), which is the check that catches a typo before deploy.

What the harness is faithful about

  • ctx.store round-trips values through JSON, because the real store does. A Date comes back as a string here exactly as it would in production.
  • Config defaults declared in the manifest are applied before your test runs, because the runtime always supplies them.
  • ctx.findings refreshes in place on (slug, key), as the store does, and rejects a name the manifest does not declare, as the host does.
  • ctx.status.set details round-trip through JSON before validation, as they do on the wire, so a NaN shows up as null and is rejected there as well.
  • A required slot must be set before a handler runs. An install cannot start with one empty, so a handler never sees one — a test that skipped h.credentials(...) would be green for a state production forbids, with undefined sitting where the generated types promise a string.