Skip to content

Repository files navigation

@silverassist/next-testing-toolkit

Integration-testing harness for npm packages that Next.js apps consume. It builds a throwaway Next app around the packed tarball, so the thing under test is what npm would actually publish.

Not a unit-test replacement. This catches the defects that live in dist/ and package.json — the ones a test importing from src/ is structurally blind to.

Why it exists

A unit test importing from src/ is structurally blind to defects that only exist in the published artifact — the kind of thing this harness catches:

Defect class Only visible when
A client component missing "use client" a Server Component imports the built file
An exports map pointing at files the build never produced Node resolves the published tarball
A bundler inlining a client module into a server-safe barrel a Server Component imports it
A value that never reaches its destination end-to-end a real form submission / user interaction

Most of these are packaging defects, and most are caught by next build alone — no browser needed.

Install

npm install -D @silverassist/next-testing-toolkit @playwright/test
npx playwright install --with-deps chromium

--with-deps matters on CI: a bare runner needs the browser's system libraries, and the download alone is not enough. Locally you can drop it if the libraries are already there.

Requires Node >= 22.

Quick start

Three files per package. Everything else is generated.

1. package.json — one script, and a port unique to this package:

{
  "scripts": {
    "e2e:setup": "next-testing-toolkit build-fixture --port 3212",
    "e2e": "npm run e2e:setup && playwright test -c e2e/playwright.config.ts"
  }
}

2. e2e/playwright.config.ts:

import { definePackageFixtureConfig } from "@silverassist/next-testing-toolkit";

export default definePackageFixtureConfig({ port: 3212 });

3. e2e/fixture/app/page.tsx — the only genuinely package-specific file. It must be a Server Component (no "use client"), because that is what turns a missing directive in your package into a build failure rather than a silent break in someone else's app:

import { MyComponent } from "@scope/my-package";

export default function Page() {
  return <MyComponent />;
}

Then npm run e2e. The harness builds your package, runs npm pack, installs the tarball into the fixture, runs next build, starts the production server and runs your specs against it.

What build-fixture does

npm run build              → your package's own build
npm pack                   → the exact tarball npm publishes
generate fixture app       → package.json, next.config.mjs, layout.tsx
npm install ./pkg.tgz      → into the fixture
next build                 → catches packaging + RSC-boundary defects

Installing the packed tarball is the whole point. Not src/, not a workspace link, not file:../ — only npm pack output exercises files, exports and the built artifact together.

Your e2e/fixture/app/page.tsx is preserved across runs; everything else in the fixture is regenerated and should be git-ignored.

Options

Flag Default Purpose
--port <n> (required) One per package, so suites can run in parallel
--next <range> ^16 Next version to install into the fixture
--react <range> ^19 React version to install
--layout-import <s> Side-effect import added to the fixture layout; repeatable

--layout-import is how a stylesheet subpath gets covered — the fixture build fails if @scope/pkg/styles stops resolving, which is how a ./styles export pointing at a file the build never emitted gets caught:

next-testing-toolkit build-fixture --port 3213 --layout-import @scope/pkg/styles

Built-output assertions

Framework-agnostic checks over dist/. Each returns results rather than throwing, so the same helpers work under Jest, Vitest or node:test.

import { checkClientBoundary } from "@silverassist/next-testing-toolkit/assertions";

for (const check of checkClientBoundary({
  clientEntry: "dist/client.js",
  rootEntry: "dist/index.js",
  clientSpecifier: "@scope/pkg/client",
})) {
  it(check.name, () => expect(check.ok).toBe(true));
}

checkClientBoundary encodes one rule:

A barrel may re-export across the RSC boundary. A bundle may not inline across it.

"use client" is a property of a module. When a bundler inlines the client module into the root barrel, the directive is flattened away and the component throws in a Server Component — while every source-level test still passes. The checks assert that the client entry carries the directive, that the root does not (marking a root that also re-exports server code would ship secret handling to the browser), and that the root re-exports rather than inlines.

checkNoClientBoundary(entries) is the inverse contract, for packages of pure server-renderable components such as an icon set: one stray hook silently converts every component into a client component and starts shipping JavaScript for static markup.

Ignore patterns

The fixture installs a tarball into its own node_modules. Without excluding it, eslint . walks into third-party code — 3670 errors, the first time a repo with ESLint adopted this harness.

// eslint.config.mjs
import { ESLINT_IGNORE_PATTERNS } from "@silverassist/next-testing-toolkit";

export default [{ ignores: [...ESLINT_IGNORE_PATTERNS] } /* … */];

The harness also generates several fixture files — layout.tsx, next.config.mjs, tsconfig.json, next-env.d.ts and the fixture package.json. Those are excluded too: this package writes them, but each consumer would lint them against its own Prettier config, so any formatting choice made here fails somewhere. e2e/fixture/app/page.tsx is deliberately not excluded — that one is hand-written per package and is the file most worth linting.

IGNORE_PATHS is the plain-path equivalent for .gitignore and .prettierignore. Those are plain-text files and cannot import a constant, so the paths have to be copied in:

# --- next-testing-toolkit (generated fixture files) ---
dist
coverage
e2e/fixture/node_modules
e2e/fixture/.next
e2e/fixture/package-lock.json
e2e/fixture/app/layout.tsx
e2e/fixture/next.config.mjs
e2e/fixture/next-env.d.ts
e2e/fixture/tsconfig.json
e2e/fixture/package.json
test-results
playwright-report

That copy is the one place this package cannot keep in sync for you — if a release adds a generated file, IGNORE_PATHS picks it up but .prettierignore will not.

Notes

Stub third-party scripts. A spec that reaches google.com is flaky in CI and tests Google, not your package. What matters is your package's own wiring: does it request the right URL, and does the value reach the element a Server Action reads?

Playwright, not Cypress. The org standardises on Cypress for Next apps; packages share no specs, helpers or fixtures with those repos, so the consistency argument does not reach them. Playwright's webServer starts and stops the fixture itself, removing the start-server-and-test dependency the Cypress path needs.

Chromium only. Next's Playwright guide describes driving Chromium, Firefox and WebKit, which is the right default for an app — cross-browser rendering is part of what an app promises. A fixture is not testing rendering: it tests that the tarball resolves, that Server Component boundaries hold, and that a value reaches a Server Action. Three of the four defects above are caught by next build alone, no browser involved. A second and third engine would triple CI time to re-verify a module graph that cannot vary by engine.

The generated config already follows Next's guidancenext start against the production build rather than next dev, a baseURL so specs use page.goto("/"), and reuseExistingServer off in CI. It binds 127.0.0.1 rather than localhost because localhost can resolve to IPv6 while the server binds IPv4, which surfaces as an unhelpful "This page couldn't load" while curl succeeds.

Pair it with packaging checks — cheap, and they find real defects:

Tool Catches
npm publish --dry-run what actually ships, before it ships
publint malformed exports, wrong fields, broken paths
@arethetypeswrong/cli types unresolvable per module-resolution mode

License

PolyForm Noncommercial 1.0.0


Made with ❤️ by Silver Assist

About

Integration-testing harness for npm packages consumed by Next.js apps: builds a fixture app around the packed tarball and asserts the React Server Components boundary.

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages