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

manten

v2.0.0

Published

満点 - Lightweight testing library for Node.js

Readme

Tests are scripts. Not a framework.

Write tests in TypeScript, run with node test.ts.

No runner, no overhead — just 16 kB.

$ node ./tests/index.ts

12:34:56 ✔ adds numbers
12:34:56 ✔ async operation (52ms)
12:34:56 ✔ Auth › login succeeds
12:34:56 ✔ Auth › logout clears session
12:34:56 ✖ broken test

523ms
4 passed
1 failed

passed · failed · skipped · pending (process exited before test finished)

Install

npm i -D manten

Why manten?

No runner, just Node

Test files are plain scripts — run them directly:

node tests/index.ts

No file discovery, no config files, no abstraction layers. Node.js startup time, nothing more.

You already know the API

sequential = await

concurrent = Remove await

That's the entire concurrency model.

await test('first', async () => { /* ... */ }) // waits

test('second', async () => { /* ... */ }) // runs immediately

test('third', async () => { /* ... */ }) // runs with second

[!TIP] Prefer concurrent by default. Only await to enforce ordering. Since Node.js won't exit while promises are settling, you don't actually need to await anything.

Standalone imports

Every API is a standalone import — no callback destructuring:

import {
    test, describe, expect, skip, onTestFinish
} from 'manten'

Each function automagically knows which test or group it belongs to.

Tiny

One dependency (expect for assertions — swap it for any assertion library).

Quick start

// tests/index.ts
import { test, expect } from 'manten'

test('adds numbers', () => {
    expect(1 + 1).toBe(2)
})

test('async operation', async () => {
    const result = await fetchData()
    expect(result).toBeDefined()
})
node tests/index.ts

[!TIP] Node.js 22.6+ runs TypeScript natively — no loaders needed.

Core concepts

Async flow control

Tests execute immediately when test() is called. Use await to control ordering:

// Sequential
await test('step 1', async () => { /* ... */ })
await test('step 2', async () => { /* ... */ }) // runs after step 1

// Concurrent
test('independent A', async () => { /* ... */ })
test('independent B', async () => { /* ... */ }) // runs with A

Grouping with describe

import { describe, test } from 'manten'

await describe('Auth', () => {
    test('login', async () => { /* ... */ }) // Auth › login
    test('logout', async () => { /* ... */ }) // Auth › logout
})

// Runs after both Auth tests complete
test('next', () => { /* ... */ })

Awaiting a group waits for all children. Groups nest infinitely.

Splitting tests across files

Import files inside describe() — their tests automatically nest under the parent group:

// tests/index.ts
import { describe } from 'manten'

describe('my-app', async () => {
    import('./auth.ts')
    import('./api.ts')
    import('./utils.ts')

    // Or add `await` to run files sequentially
})
// tests/auth.ts
import { describe, test, expect } from 'manten'

describe('Authentication', () => {
    test('login', () => { /* ... */ })
    test('logout', () => { /* ... */ })
    test('refresh token', () => { /* ... */ })
})
// Output: my-app › Authentication › login
//         my-app › Authentication › logout
//         my-app › Authentication › refresh token

Each file works standalone too — node tests/auth.ts runs just that file. The entry point is your test runner, written in plain JavaScript.

Parameterized test files

To pass data into a test file, export a function that wraps a describe():

// tests/specs/builds.ts
import { describe, test, expect } from 'manten'

export const builds = (nodePath: string) => describe('builds', () => {
    test('compiles', async () => {
        const result = await run(nodePath)
        expect(result.exitCode).toBe(0)
    })
})

Since the describe() doesn't run until the function is called, these can be statically imported:

// tests/index.ts
import { builds } from './specs/builds.ts'
import { errors } from './specs/errors.ts'
import { describe } from 'manten'

describe('my-app', async () => {
    for (const nodeVersion of ['v20', 'v22', 'v24']) {
        const node = await getNode(nodeVersion)
        await describe(`Node ${node.version}`, () => {
            builds(node.path)
            errors(node.path)
        })
    }
})

Recommended project structure

tests/
  index.ts     # entry point — run this
  specs/       # test files
  utils/       # shared test helpers
  fixtures/    # static test data

Use a single index.ts entry point that imports all test files. This gives you one command to run everything and enables node --watch across all files.

Watch mode

node --watch tests/index.ts

Built into Node.js (stable since v22). Watches all imported files — change any test file and tests re-run automatically. This is why a single entry point matters: one command watches your entire test suite.

Features

Timeouts & abort signals

Pass a timeout (ms) as the third argument. The test receives an AbortSignal for cooperative cancellation:

test('fetch with timeout', async ({ signal }) => {
    await fetch('https://api.example.com', { signal })
}, 5000)

For multi-step tests, use signal.throwIfAborted() between operations. Combine with your own signals using AbortSignal.any().

Retries

test('flaky API', async () => {
    await unreliableAPI()
}, {
    timeout: 5000,
    retry: 3
})

Output shows which attempt succeeded: ✔ flaky API (2/3).

Hooks

import { test, onTestFail, onTestFinish } from 'manten'

test('with cleanup', async () => {
    const resource = await acquire()
    onTestFinish(() => resource.cleanup()) // runs after test (pass or fail)
    onTestFail(error => console.log('Debug:', error))
})

onFinish runs after all tests in a describe():

import { describe, test, onFinish } from 'manten'

describe('Database', async () => {
    const database = await connect()
    onFinish(() => database.close())

    test('query', () => { /* ... */ })
})

Skipping

import { test, skip } from 'manten'

test('linux only', () => {
    if (process.platform !== 'linux') {
        skip('Only runs on Linux')
    }
    // ...
})

Skip entire groups — skip() must be called before any test() or nested describe():

describe('GPU tests', () => {
    if (!hasGPU) {
        skip('GPU not available')
    }
    test('render shader', () => { /* ... */ }) // all skipped
})

Snapshot testing

import { test, expectSnapshot } from 'manten'

test('user state', () => {
    // Named (recommended) — order-independent
    expectSnapshot(getUser(), 'initial state')

    // Auto-numbered — keys become "user state 1", "user state 2", etc.
    expectSnapshot(getUser())
    expectSnapshot(getPermissions())
})

Snapshots are stored in .manten.snap. Update with MANTEN_UPDATE_SNAPSHOTS=1 node tests/index.ts. Without named snapshots, reordering expectSnapshot() calls breaks comparisons.

[!WARNING] Snapshots are serialized with util.inspect, which may produce different output across Node.js versions. If snapshots fail after upgrading Node, re-run with MANTEN_UPDATE_SNAPSHOTS=1 to regenerate.

Concurrency limiting

describe('Database tests', () => {
    test('query 1', async () => { /* ... */ })
    test('query 2', async () => { /* ... */ })
    test('query 3', async () => { /* ... */ })
}, { parallel: 2 }) // max 2 concurrent

Options:

  • false (sequential)
  • true (unbounded)
  • number (limit)
  • 'auto' (adapts to CPU load)

Tests that you explicitly await run immediately, bypassing the parallel queue — useful for setup/teardown steps within a parallel group.

Group timeouts

describe('API suite', () => {
    test('endpoint 1', async () => { /* ... */ })
    test('endpoint 2', async () => { /* ... */ })
}, { timeout: 10_000 })

Individual test timeouts still apply — whichever is stricter wins.

Process timeout

Prevent stuck processes in CI:

import { setProcessTimeout } from 'manten'

setProcessTimeout(10 * 60 * 1000) // kill after 10 minutes

Filtering

Run specific tests by substring match (case-sensitive). Matches against the full title including describe prefixes:

TESTONLY='login' node tests/index.ts
TESTONLY='Auth' node tests/index.ts   # matches "Auth › login", "Auth › logout", etc.

API

test(name, fn, timeoutOrOptions?)

Create and run a test. fn always receives { signal } — an AbortSignal that aborts on timeout or when the parent group is aborted.

  • timeoutOrOptions: number | { timeout?: number, retry?: number }
  • Returns: Promise<void>

describe(name, fn, options?)

Create a test group. fn always receives { signal } — an AbortSignal that aborts on timeout or when the parent group is aborted.

  • options: { parallel?: boolean | number | 'auto', timeout?: number }
  • Returns: Promise<void>

expect(value)

Jest's expect. Or use Node.js Assert, Chai, etc.

expectSnapshot(value, name?)

Compare against a stored snapshot. Creates one if none exists. Test names must be unique across all files — duplicates throw an error.

onTestFail(callback) · onTestFinish(callback)

Hooks for the current test. Must be called within test(). Hook errors are logged but don't fail the test.

onFinish(callback)

Cleanup hook for the current describe() group. Errors are logged and set process.exitCode = 1.

skip(reason?)

Skip the current test or describe group.

setProcessTimeout(ms)

Global timeout for the entire process.

configure(options)

{ snapshotPath?: string } — must be called before any expectSnapshot(). Also configurable via MANTEN_SNAPSHOT_PATH and MANTEN_UPDATE_SNAPSHOTS env vars.

TypeScript

Manten is written in TypeScript. All APIs are fully typed, and Test/Describe types are exported for advanced use cases.

FAQ

What does manten mean?

Manten (まんてん, 満点) means "maximum points" or 100% in Japanese.

Why no test runner?

No runner = zero overhead. No file discovery, no spawning processes, no config. Tests are scripts — run them however you want.

Why no beforeAll/beforeEach?

Manten runs tests concurrently by default. Shared setup hooks don't compose with concurrent execution. Inline setup in each test, or use describe() + onFinish() for shared resources.

How does manten report failures to CI?

When a test fails, manten sets process.exitCode = 1 but doesn't force-exit. All remaining tests run to completion, and the final report prints on the exit event. CI systems pick up the non-zero exit code automatically.

Related

fs-fixture

Create disposable file system fixtures for testing. Pairs naturally with manten's hooks:

import { createFixture } from 'fs-fixture'
import { test, expect } from 'manten'

test('reads config', async () => {
    await using fixture = await createFixture({
        'package.json': JSON.stringify({ name: 'my-app' }),
        'src/index.js': 'export default 42'
    })

    const result = await readPackageJson(fixture.path)
    expect(result.name).toBe('my-app')
}) // fixture auto-cleaned up when test scope exits

Sponsors