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

@usehenri/testing

v1.2.0

Published

henri testing helpers: boots the app for vitest and binds a supertest agent to it

Readme

@usehenri/testing

Boots a henri app for its tests and binds supertest to the running server. Built for Vitest, which henri test runs.

pnpm add -D vitest @usehenri/testing

Setup

vitest.config.js in the app:

const { defineConfig } = require('vitest/config');

module.exports = defineConfig({
  test: {
    environment: 'node',
    fileParallelism: false,
    globals: true,
    include: ['test/**/*.{spec,test}.js'],
    setupFiles: ['@usehenri/testing/setup-file'],
  },
});

The setup file boots henri (NODE_ENV=test, config/test.json, a free port, workers skipped) before each test file and stops it after, inside the test worker: henri and the models are globals in the tests, exactly like in the app. fileParallelism: false keeps one server and one database at a time -- drop it only when each file gets a database of its own, which is what the disk adapter does under NODE_ENV=test.

Writing tests

// test/tasks.spec.js
const { request } = require('@usehenri/testing');

describe('tasks', () => {
  test('lists tasks', async () => {
    await Task.create({ title: 'write tests' });

    const res = await request().get('/tasks').set('Accept', 'application/json');

    expect(res.status).toBe(200);
    expect(res.body.tasks).toHaveLength(1);
  });
});

Without the setup file, boot henri from the test file itself; setup() is a no-op when henri is already running:

const { request, setup, teardown } = require('@usehenri/testing');

beforeAll(() => setup());
afterAll(() => teardown());

Factories

A factory makes a valid record with the fields the test does not care about already filled in. It lives in test/factories/<name>.js, is named after its file and writes to the model of that name.

// test/factories/task.js
module.exports = {
  attributes: {
    name: ({ sequence }) => `Task ${sequence}`,
    ownerId: async ({ create }) => (await create('user')).id,
  },

  traits: {
    done: { completedAt: () => new Date(), state: 'done' },
  },
};
const { build, create, createList } = require('@usehenri/testing');

await create('task'); // saved, with an owner
await create('task', 'done'); // with the trait
await create('task', { ownerId: me.id }); // no second user is made
await createList('task', 3, 'done');
await build('task'); // the attributes, unsaved

A value is a literal or a function of the build context (attrs, build, create, sequence, traits, uid), and fields resolve on demand: reading await attrs.eventId from another field resolves that one first. An override always wins, and the definition's value is then not evaluated at all.

after(record, context) runs on the saved record and may replace it, which is how a factory grants roles the model will not mass assign. defineFactory(name, definition) declares one from a test file.

Mail and jobs

inbox() is the mail the application was asked to send, oldest first, and enqueued() is what the job queue holds:

const { enqueued, inbox, request } = require('@usehenri/testing');

await request().post('/signup').send({ email: '[email protected]', password });

const [mail] = inbox();

expect(inbox()).toHaveLength(1);
expect(mail.to).toContain('[email protected]');
expect(mail.subject).toBe('Confirm your address');
expect(mail.html).toContain('/confirm/');

expect(await enqueued('welcome')).toHaveLength(1);

Both doors are captured: deliver(), which reaches the transport, and deliverLater(), which reaches the queue -- an entry says which with deferred, and carries to, cc, bcc, from, subject, html, text, the mailer and action that rendered it, and message, the nodemailer payload itself. inbox(filter) narrows on action, deferred, mailer, subject and to, or on a predicate. The setup file empties the inbox before every test; clearInbox() does it by hand.

enqueued(filter) reads the queue back, so a row's args are what a runner will claim; henri.jobs.performNow(job.name, job.args) runs one. It needs @usehenri/jobs, which this package does not depend on: an application without a queue is told what to install rather than handed an empty list. clearJobs() forgets the rows, and nothing calls it for you.

API

  • setup({ workers = false }) boots henri for the app in process.cwd() and resolves with the instance. Idempotent.
  • teardown() stops it.
  • request() a supertest request bound to the running server.
  • agent() a supertest agent (keeps cookies between requests).
  • inbox(filter) the mail the app was asked to send; clearInbox() empties it.
  • enqueued(filter) the jobs the queue holds; clearJobs(filter) forgets them.
  • create(name, ...traits, overrides) a saved record from a factory.
  • build(name, ...traits, overrides) the attributes, without saving.
  • createList(name, count, ...traits, overrides) several of them.
  • defineFactory(name, definition) a factory without a file.
  • resetFactories() forgets the definitions and the sequences.
  • henri the running instance (also global.henri).
  • supertest the underlying module.

One server for the whole run

globalSetup: ['@usehenri/testing/global-setup'] boots henri once in Vitest's main process. Tests only get HTTP access through request() (no henri or model globals in the workers) since global setup runs in another process.

Browser tests

@usehenri/testing/playwright is the same boot under Playwright, and the whole of what an application writes for one:

// playwright.config.js
module.exports = {
  globalSetup: '@usehenri/testing/playwright',
  testDir: './test/browser',
};

The port is the kernel's answer to listen(0), so it does not exist when playwright.config.js is read. The global setup boots henri and exports PLAYWRIGHT_TEST_BASE_URL, which is what Playwright's use.baseURL falls back to in every worker — measured against Playwright 1.63.0, along with the rest of that precedence: anything written in use wins over the variable, and henri says so rather than letting the browser go somewhere else in silence.

Playwright is the application's dependency: this package never imports it, and henri doctor reports a playwright.config.* with no @playwright/test in package.json. The database is left alone — one server for the run is one database for the run, so seeding belongs to a global setup of your own that wraps boot(config) from this subpath. See the guide.

henri test                   # vitest run
henri test --watch           # vitest in watch mode
henri test test/tasks.spec.js -t "lists"