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

@lokalise/universal-testing-utils

v5.0.1

Published

Readme

universal-testing-utils

Reusable testing utilities that are potentially relevant for both backend and frontend

Helpers

| Helper | Mock server | |---|---| | ApiContractMockttpHelper | mockttp | | ApiContractMswHelper | msw |

Both helpers work with contracts defined via defineApiContract from @lokalise/api-contracts. The short names are aliases of the ApiContract* classes and behave identically.

Table of contents

ApiContractMockttpHelper

Mock HTTP responses in mockttp-based tests using contracts defined with defineApiContract from @lokalise/api-contracts.

Setup

import { getLocal } from 'mockttp'
import { ApiContractMockttpHelper } from '@lokalise/universal-testing-utils'

const mockServer = getLocal()
const helper = new ApiContractMockttpHelper(mockServer)

beforeEach(() => mockServer.start())
afterEach(() => mockServer.stop())

mockResponse

Registers a mock rule for the given contract. responseStatus is the concrete numeric HTTP status code the mock will send (e.g. 201, 404). It also controls which schema is used: the helper looks up the contract entry with exact → range → 'default' precedence, so a contract with only a '2xx' key accepts any responseStatus in 200–299.

await helper.mockResponse(contract, params)

params is a discriminated union on responseStatus. The required body fields are inferred from the contract's response type for that status code:

| Response type | Required field | |---|---| | ZodType (JSON) | responseJson: z.input<T> | | sseResponse(schemas) | events: { event: string; data: unknown }[] | | blobResponse(contentType) | responseBlob: string | | noBodyResponse() | (none) | | content map with JSON and SSE entries | responseJson + events |

Path params are required when the contract declares requestPathParamsSchema, and optional otherwise.

JSON response

const contract = defineApiContract({
  method: 'get',
  pathResolver: () => '/users',
  responsesByStatusCode: { 200: z.object({ id: z.string() }) },
})

await helper.mockResponse(contract, {
  responseStatus: 200,
  responseJson: { id: '1' },
})

The response body is validated and stripped through the contract's Zod schema before being sent.

JSON response with path params

const contract = defineApiContract({
  method: 'get',
  requestPathParamsSchema: z.object({ userId: z.string() }),
  pathResolver: ({ userId }) => `/users/${userId}`,
  responsesByStatusCode: { 200: z.object({ id: z.string() }) },
})

await helper.mockResponse(contract, {
  pathParams: { userId: '42' },
  responseStatus: 200,
  responseJson: { id: '42' },
})

No-body response

const contract = defineApiContract({
  method: 'delete',
  requestPathParamsSchema: z.object({ userId: z.string() }),
  pathResolver: ({ userId }) => `/users/${userId}`,
  responsesByStatusCode: { 204: ContractNoBody },
})

await helper.mockResponse(contract, {
  pathParams: { userId: '1' },
  responseStatus: 204,
})

SSE response

const contract = defineApiContract({
  method: 'get',
  pathResolver: () => '/events/stream',
  responsesByStatusCode: {
    200: sseResponse({ 'item.updated': z.object({ id: z.string() }), completed: z.object({ totalCount: z.number() }) }),
  },
})

await helper.mockResponse(contract, {
  responseStatus: 200,
  events: [
    { event: 'item.updated', data: { id: '1' } },
    { event: 'completed', data: { totalCount: 1 } },
  ],
})

Dual-mode response (SSE + JSON)

Contracts whose content map declares both application/json and text/event-stream serve either SSE or JSON depending on the request's Accept header. Both events and responseJson are required so the mock can respond to either mode.

const contract = defineApiContract({
  method: 'post',
  requestBodySchema: z.object({ name: z.string() }),
  pathResolver: () => '/jobs',
  responsesByStatusCode: {
    200: {
      content: {
        'application/json': z.object({ id: z.string() }),
        'text/event-stream': sseBody({ completed: z.object({ totalCount: z.number() }) }),
      },
    },
  },
})

await helper.mockResponse(contract, {
  responseStatus: 200,
  responseJson: { id: '1' },
  events: [{ event: 'completed', data: { totalCount: 1 } }],
})
  • Requests with Accept: text/event-stream receive the SSE stream.
  • All other requests receive the JSON body.

Selecting a content type

When a status code declares multiple content types, pass contentType to pin the mock to one specific entry — only that entry's body field is required, and the response always uses that content type (no Accept negotiation). This is the only way to mock an entry that negotiation would never pick, e.g. a second JSON content type or a blob entry that sits next to a JSON one.

const contract = defineApiContract({
  method: 'get',
  pathResolver: () => '/report',
  responsesByStatusCode: {
    200: {
      content: {
        'application/json': z.object({ id: z.string() }),
        'application/problem+json': z.object({ title: z.string(), detail: z.string() }),
      },
    },
  },
})

await helper.mockResponse(contract, {
  responseStatus: 200,
  contentType: 'application/problem+json',
  responseJson: { title: 'Invalid', detail: 'Something went wrong' },
})

Without contentType, the existing behavior applies: SSE is served when the request negotiates it via Accept, otherwise the first JSON entry wins, then blob.

Range and wildcard status keys

Contracts may use range keys ('1xx'–'5xx') or 'default' in responsesByStatusCode instead of exact codes. Pass any concrete numeric code covered by that range as responseStatus; the helper resolves the contract entry using the same exact → range → 'default' precedence as the runtime client.

Range key only — responseStatus accepts any code in 200–299:

const contract = defineApiContract({
  method: 'get',
  pathResolver: () => '/items',
  responsesByStatusCode: { '2xx': z.object({ id: z.string() }) },
})

await helper.mockResponse(contract, {
  responseStatus: 201,          // any 2xx code is valid
  responseJson: { id: '1' },
})

'default' catch-all — responseStatus accepts any HttpStatusCode:

const contract = defineApiContract({
  method: 'get',
  pathResolver: () => '/items',
  responsesByStatusCode: { default: z.object({ id: z.string() }) },
})

await helper.mockResponse(contract, {
  responseStatus: 200,
  responseJson: { id: '1' },
})

Exact key takes priority — when both 200 and '2xx' are defined, responseStatus: 200 uses the exact entry and responseStatus: 201 falls back to the range entry:

const contract = defineApiContract({
  method: 'get',
  pathResolver: () => '/items',
  responsesByStatusCode: {
    200: z.object({ id: z.string() }),
    '2xx': z.object({ id: z.string(), created: z.literal(true) }),
  },
})

await helper.mockResponse(contract, { responseStatus: 200, responseJson: { id: '1' } })
await helper.mockResponse(contract, { responseStatus: 201, responseJson: { id: '2', created: true } })

How StatusCodeBodyPair works (type-level)

MockResponseParams<TContract> is a discriminated union on responseStatus. It has two branches:

  • ExactStatusCodePairs — one member per exact numeric key in responsesByStatusCode. responseStatus is that literal number and the body fields come from the entry at that key.
  • RangeStatusCodePairs — one member per wildcard key ('1xx'–'5xx', 'default'). ExpandStatusRangeKey<K> expands the key to its numeric union (e.g. '2xx' → 200|201|…|299), then exact codes already covered by ExactStatusCodePairs are excluded via Exclude so the discriminated union stays unambiguous.

mockResponseWithImplementation

mockResponse takes a fixed body. When the response has to depend on what the caller sent, use mockResponseWithImplementation and return the body from a handler.

responseStatus still selects the contract entry, which is what types handleRequest's return value and supplies the Zod schema the result is validated against. The handler's request argument is typed from the contract too, so request.body.getJson() (mockttp) and request.json() (msw) hand back the output of the contract's requestBodySchema rather than unknown. Only JSON entries are addressable this way: SSE and blob responses stay static, via mockResponse.

// mockttp: the handler receives the mockttp CompletedRequest
await helper.mockResponseWithImplementation(postUserContract, {
  responseStatus: 200,
  handleRequest: async (request) => {
    const body = await request.body.getJson() // typed by requestBodySchema
    return { id: `id-${body.name}` }
  },
})

// msw: the handler receives the msw request info
helper.mockResponseWithImplementation(postUserContract, {
  responseStatus: 200,
  handleRequest: async ({ request }) => {
    const body = await request.json() // typed by requestBodySchema
    return { id: `id-${body.name}` }
  },
})

// with path params
await helper.mockResponseWithImplementation(getUserContract, {
  pathParams: { userId: '7' },
  responseStatus: 200,
  handleRequest: (request) => ({ id: request.path.split('/').pop() ?? '' }),
})

Picking the media type with contentType

The response goes out under the media type the contract declares it for, so an application/problem+json entry is served as application/problem+json rather than application/json. When a status entry declares more than one JSON media type, pass contentType to say which one the handler is answering with; the selected descriptor then types the handler's result:

const contract = defineApiContract({
  method: 'get',
  pathResolver: () => '/items',
  responsesByStatusCode: {
    200: {
      content: {
        'application/json': z.object({ id: z.string() }),
        'application/problem+json': z.object({ title: z.string(), detail: z.string() }),
      },
    },
  },
})

await helper.mockResponseWithImplementation(contract, {
  responseStatus: 200,
  contentType: 'application/problem+json',
  handleRequest: () => ({ title: 'Invalid', detail: 'Something went wrong' }),
})

contentType is required whenever the choice would be ambiguous and optional otherwise. It only names JSON entries: pointing it at an SSE or blob descriptor throws, as does a status entry with no JSON body at all. A status that also declares text/event-stream still serves JSON here, but a request negotiating the SSE branch through Accept gets a 406 rather than a JSON body it cannot read. Mock that branch with mockResponse.

Per-call status codes with response()

By default every call replies with responseStatus. To vary it per call, wrap the returned body with the helper's static response():

let callCount = 0
await helper.mockResponseWithImplementation(getUserContract, {
  responseStatus: 200,
  handleRequest: () => {
    callCount++
    if (callCount === 1) {
      return ApiContractMockttpHelper.response({ message: 'nope' }, { status: 404 })
    }
    return { id: 'second' } // a plain body still replies with responseStatus
  },
})

An overridden status selects its own contract entry, so the wrapped body is validated against the schema declared for the status actually being sent: { message: 'nope' } above has to satisfy the contract's '4xx' entry, not its 200 one. Overriding to a status the contract does not declare is an error.

ApiContractMswHelper.response() is the msw equivalent and returns the same wrapper.

Status code priority: response({ status }) > responseStatus.

When a handler fails

mockttp and msw both turn a throwing route callback into a bare 500, which reaches the test as a client-side parse failure with the cause nowhere in sight. A handler that throws, or that returns a body the contract schema rejects, instead produces a 500 whose body carries the reason, logged through console.error alongside the contract it came from:

[ApiContractMockttpHelper.mockResponseWithImplementation] POST /users/:userId: ZodError: ...

Setup-time problems (an unmapped status, a status with no JSON body, an ambiguous or unmatched contentType) still throw from mockResponseWithImplementation itself, where the stack points at the test that set the mock up.

Type safety

MockResponseParams<TContract> is exported for cases where you need to type the params object separately:

import type { MockResponseParams } from '@lokalise/universal-testing-utils'

function mockUser(params: MockResponseParams<typeof getUserContract>) {
  return helper.mockResponse(getUserContract, params)
}

ApiContractMswHelper

The msw-based counterpart to ApiContractMockttpHelper for contracts defined with defineApiContract. mockResponse accepts the same MockResponseParams and follows the same rules — response entry resolution with exact → range → 'default' precedence, Zod validation of responseJson, SSE/JSON negotiation via the Accept header, blob and no-body entries. The only differences are the setup (an msw SetupServer plus a base URL, since msw matches absolute URLs) and that mockResponse is synchronous.

import { setupServer } from 'msw/node'
import { ApiContractMswHelper } from '@lokalise/universal-testing-utils'

const server = setupServer()
const helper = new ApiContractMswHelper(server, 'http://localhost:8080')

beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
afterEach(() => server.resetHandlers())
afterAll(() => server.close())

helper.mockResponse(contract, {
  responseStatus: 200,
  responseJson: { id: '1' },
})

mockResponseWithImplementation and the static response() are available here too, with handleRequest receiving msw's request info instead of a mockttp CompletedRequest.

formatSseResponse

A standalone helper exported for manual SSE response formatting:

import { formatSseResponse } from '@lokalise/universal-testing-utils'

const body = formatSseResponse([
  { event: 'item.updated', data: { items: [{ id: '1' }] } },
  { event: 'completed', data: { totalCount: 1 } },
])
// "event: item.updated\ndata: {\"items\":[{\"id\":\"1\"}]}\n\nevent: completed\ndata: {\"totalCount\":1}\n"