Run commands from the repository root. Node.js 22 and 24 are supported.
Use the npm test commands below, or npm run test:run -- to pass arguments directly to Jest. All Jest invocations from these commands, pre-push and inventory checks use scripts/test-run.mjs. On macOS arm64 this wrapper launches Jest with --no-sparkplug; Linux and Windows keep Node's default JIT settings.
The observed Node 24 crashes contain Builtins_BaselineOutOfLinePrologue and ClearStaleLeftTrimmedPointerVisitor, matching the Sparkplug report in nodejs/node#62393. The earlier --no-maglev workaround did not prevent the crash. Successful repetitions measure observed stability; they do not prove a rare native fault impossible. Keep the workaround scoped to the reproduced environment and reassess it when the upstream fix is verified. Jest workers inherit the flag through execArgv; CLI fixture child processes and real browsers keep their normal settings. Tests are never automatically retried or skipped to hide a native crash.
| Command | Checks | Builds the package |
|---|---|---|
npm test |
Every Jest project, including both browsers | Once |
npm run test:unit |
Node and DOM unit tests | No |
npm run test:watch |
Node and DOM unit tests in watch mode | No |
npm run test:build |
Compiler plugins, the built CLI and build integrations | Once |
npm run test:types |
Jest declaration consumer tests | Once |
npm run test:browser |
Chrome and Firefox scenarios | Once |
npm run test:browser:chrome |
Chrome scenarios | Once |
npm run test:browser:firefox |
Firefox scenarios | Once |
npm run test:inventory |
Test ownership, local mocks and framework reset inventory | No |
npm run test:run -- … |
Direct Jest arguments; caller selects groups and prepares dist | No |
npm run typecheck checks framework and test-runner types. npm run fixtures:prepare builds the package and prepares
fixture applications for editor/manual use. npm run typecheck:fixtures builds the package, prepares every fixture
application and checks it with its own tsconfig.json. Declaration consumers without an application config belong to
the Jest types project and run through test:types; they are separate from fixture application typechecks.
npm run check runs framework typechecking, one package build, all non-browser Jest projects and fixture preparation/typechecks.
Husky pre-push uses this same command. It does not launch browsers or collect coverage.
Select a feature by path instead of adding a script for each module:
npm run test:unit -- --testPathPatterns=src/locale
npm run test:unit -- --testPathPatterns=src/relay
npm run test:unit -- --runTestsByPath src/sandbox/SandboxMessage.test.ts
npm run test:watch -- --testPathPatterns=src/message
npm run test:unit -- --coverageThe selected group remains the boundary: test:unit never includes build or browser tests even when their paths match.
Commands that launch Jest forward arguments after -- to Jest. Fixture preparation, fixture typechecking and check are separate
workflows, not Jest argument forwarders. Public build-dependent test commands always build first. CI and check reuse
one explicit build and invoke test:run (or its underlying wrapper) directly, avoiding nested build commands.
The 2026-09-25 benchmark records final migration timings, raw runs and comparison limits. The Relay type-checking follow-up records the subsequent constructor inference fix. The override scheduling follow-up compares the unchanged build matrix at eight and two workers.
jest.config.ts defines six projects: unit-node, unit-dom, build, types, chrome and firefox. Put DOM tests in the unit-dom patterns. Tests that execute the built CLI, consume package declarations or compile imports from the built package belong to build or types. Keep unit tests runnable with no dist directory. Each browser file contains scenarios for one browser only.
Relay declaration tests keep the source/package × POSIX/Windows matrix for the generated registry, selectors and exports.
Constructor, prepared-data and bootstrap contracts live in integration/types/relay.integration.test.ts, checked against
both source aliases and the built package. Only its source case also checks the generated virtual entry module.
After a package build, direct Jest invocations can reuse that build:
npm run build
npm run test:run -- --selectProjects build types --maxWorkers=4
npm run test:run -- --selectProjects chrome --maxWorkers=4Jest uses up to eight workers, leaving one CPU available on smaller machines. Override the pool with -- --maxWorkers=N, or use -- --runInBand for a serial diagnostic run. The limit applies to the whole Jest invocation; do not pass both flags together. CI sets explicit smaller pools for compiler and browser jobs.
The trailing --verbose=false in group commands terminates Jest's variadic --selectProjects option so an appended test filename is treated as a filename, not another project name. test:watch terminates it with --watch.
Preparation and fixture typechecks use a bounded queue of up to four processes. Set ADNBN_TEST_WORKERS to a positive integer to override it. Each application keeps its own configuration and TypeScript process. Queued builds and typechecks allow up to 120 seconds per command. Builds inside Jest scenarios retain the 30-second limit so a hung CLI does not outlive its enclosing test deadline. A failure is reported after all running work has finished, with the child process diagnostics preserved.
See integration/README.md for browser requirements and fixture layout. Browser tests use separate profiles, ports and application copies. Tests that edit or watch a fixture must retain their own copy and build; generated outputs must not be shared between mutable scenarios.
tests/jest.setup.ts installs the browser harness for every unit, build and types test. Real Chrome/Firefox
integrations load no setup. Each test gets real Browser/Storage modules unless it declares an inventoried local mock.
There is no global module mock or per-file setup selection.
browser-harness/local-mocks.json records local jest.mock, jest.doMock, jest.setMock and jest.unstable_mockModule
calls for @addon-core/* and @main/env. The list is currently empty. Any future package mock requires an explicit inventory entry and review.
Passing under harness setup does not prove that a locally mocked dependency was exercised. Review any future exception
against the available Browser/Storage controls and document the dependency boundary it represents.
npm run test:inventory rejects missing files and unrecorded or removed module mocks.
Update the inventory alongside test changes so stale exceptions cannot remain unnoticed.
The harness setup creates a fresh BrowserTestSession before every test. Use getBrowserTest() from
@tests/browser-harness/session inside tests and hooks, not at module scope. The @tests/* alias maps to the root tests/
directory in TypeScript and Jest. Browser and Storage imports are real unless the file declares a recorded local mock;
build-time virtual modules can still supply explicit test fixtures. NativeLocale.test.ts demonstrates API controls
and call history instead of function mocks.
Locale tests configure the raw i18n API and exercise the real Storage package over harness storage, including namespaced
keys, external changes and failures. MemoryLocaleStorage remains a custom-driver fixture for controlled pending
writes and driver-sharing contracts. Build-generated #adnbn/locale, #adnbn/page and #adnbn/runtime retain explicit
fixtures; they are not Browser/Storage replacements. Content node tests inject the styles runtime through that generated
facade and use the real Browser URL helper.
import {getBrowserTest} from "@tests/browser-harness/session";
const session = getBrowserTest();
session.harness.configurable.chrome.i18n.getMessage.setResult("en");
session.addCleanup(unsubscribe);The default messaging context is an extension page with the Chrome profile. The session sets BROWSER=chrome,
APP=test and MANIFEST_VERSION from its manifest. Constructor options profile, app and manifest select alternatives.
Teardown restores the previous environment values, including previously absent variables. Framework environment
getters read these values when called.
Create other contexts through session.harness.contexts.create() and install them with session.useContext(context).
Use session.useContext(context, "firefox") to switch both browser globals and BROWSER; configure the reported browser
version through harness.runtime.getBrowserInfo.setResult(). The returned restore function and nested installations
follow reverse order; teardown restores remaining installations automatically. Use context-bound facades from
harness.messaging.forContext() for the other endpoints of a conversation. Global context changes are not async-local:
do not use test.concurrent or assume that global Browser wrappers remember the context of an earlier callback.
environment: "preserve" retains jsdom's window, document, location and navigator; it does not simulate a background DOM.
session.createScriptRuntime({clock: true}) binds a persistent guest runtime to the session's document registry and
registers disposal. Create the target tabs/documents first. Advance the guest clock explicitly; host timers and
consumer deadlines remain separate. Runtime and tab message delivery continue under Jest host fake timers without
advancing them. harness.delays.downloadValidation uses a host timer: advance it explicitly in tests using fake timers.
Shared teardown restores real host timers even when cleanup fails. Guest-clock checks use real timers inside the guest;
Jest fake timers affect only the host.
Register consumer subscriptions, observers and other resources with session.addCleanup(). Async cleanup is awaited
in reverse order, and a failure does not prevent remaining cleanup, harness reset or global/environment restoration.
Teardown clears Message/Relay/Offscreen/Service/Sandbox managers and Relay permission registration. It resets
NativeLocale, Message, ObservableLocale (default, memory and driver caches), and OffscreenBridge singleton references,
and disposes cached SandboxMessage hosts. Clearing a cache does not dispose every previously returned provider or DOM
bridge: subscriptions, pending DOM work and manually created instances still need explicit cleanup callbacks.
Do not reset the harness underneath a live session; that invalidates contexts and detaches the scripting executor.
Jest mock history and spies are reset centrally.
framework-state-inventory.json records source classes with static fields or getInstance, including retained constants
and the CLI TypeScript resolver cache. test:inventory compares the source fields with that inventory, checks registration
of the 11 runtime reset owners, and checks all six exported *GlobalKey declarations against the reset implementation.
New fields or owners require a reviewed reset policy. After runtime cleanup, any remaining own global property starting
with adnbn fails teardown rather than being silently deleted. This source inventory detects missing policies; it does
not prove that every reset implementation is correct, so session lifecycle tests verify the actual behavior too.
hasListenersis not modeled. Check subscriptions through the context event'slistenerCount().- Offscreen has no post-creation hook.
ProxyOffscreen.test.tstemporarily interceptscontexts.createwith a passthrough spy to register the real transport before the first message is sent. Replace this interception when the kit provides a hook such asoffscreen.onCreated(context)that completes beforecreateDocumentresolves.beforeCreatecannot serve this purpose because the receiving context does not exist yet. - VM scenarios require uninstrumented injected code. Only
src/relay/adapters/invoke-relay.tsis excluded from Babel coverage in every Jest project. Its host adapter remains instrumented; VM tests run the real bundled Relay manager, and Chrome MV3 plus Firefox MV2/MV3 integrations check the injected function in actual extensions. - Native Offscreen unit tests attach the real
RegisterOffscreentransport when the harness creates its context and dispose it with that context. The kit owns creation, closure and messaging; it does not execute the extension HTML.tests/offscreen/MockLockManager.tsmodels the Web Locks boundary for scheduling tests. Firefox/MV2 bridge tests use jsdom frames and explicit ready/error events; real-browser integrations remain the lifecycle check. - Sandbox messaging uses
postMessage, not WebExtension runtime messaging.SandboxMessage.test.tsuses the realSandboxMemoryport pair and real request IDs, with channels registered for session cleanup. Fake-timer tests cover request deadlines, disposal and responses arriving out of order; DOM/provider and real-browser tests cover frame wiring. - The session does not emulate Web Locks, DOM inside the guest, or extension HTML execution. Keep explicit external adapters where needed and real-browser checks for DOM, execution worlds, browser lifecycle and vendor behavior.
- Consult the kit's
RAW_CAPABILITY_COVERAGEbefore using another browser API. Extend the kit when a required capability is missing; an existing modeled capability needs no new global mock.
Pre-commit checks only formatting of the staged contents, without changing the index or unstaged work. Format reported files and stage them before retrying. Tests, typechecking and builds run at pre-push and in CI. During development, run test:unit or test:watch explicitly for earlier feedback. Tests execute the working tree, so CI remains the full validation of the committed revision.
Pre-push runs npm run check, without browser launches or coverage. It includes both framework and fixture typechecks, compiler and CLI checks, and declaration consumers. Framework typechecking runs alongside the package build; after the build, Jest runs alongside the fixture queue. Jest inherits the terminal and streams its output live; fixture diagnostics remain buffered per command. Each fixture is typechecked as soon as its own build finishes. All tasks finish before failures are reported. Each task has a five-minute deadline; timeout or interruption terminates its complete process tree, including wrappers, Jest workers and child CLI processes. This uses a separate process group on macOS/Linux and taskkill /T /F on Windows. Browser checks run in CI and are also available through the explicit commands above.
CI runs units, compiler/build/declaration tests, fixture typechecks, Chrome and Firefox in separate jobs. Regular pushes and pull requests targeting develop or feature/** use Node 24 on Linux and Windows. Pull requests targeting main use the full Linux/Windows × Node 22/24 matrix. The release workflow also requests this full matrix with full: true and waits for it before publishing on Node 24. Reusable calls without full: true use the regular matrix unless the triggering event is a pull request targeting main. Both browsers run on Linux for each selected Node version. Linux jobs use ubuntu-24.04.
Test and Build check names retain their ubuntu-latest label and the (full matrix) suffix for full runs. They aggregate the entire selected matrix, not only the OS/Node named in each check, and fail if any required job fails, is cancelled or is skipped. A regular workflow contains 12 jobs, including 8 test jobs; a full workflow contains 22 jobs, including 16 test jobs. These counts include matrix computation, coverage merging and aggregation. Node 22 remains supported by the package.
Babel coverage is collected in the Ubuntu/Node 24 unit and build jobs, then merged once with scripts/merge-coverage.mjs. The other matrix cells run the same tests without coverage instrumentation. Both regular and full runs publish coverage-ubuntu-24.04-node24; platform-specific coverage reports are no longer produced. Moving compiler tests between groups must not remove their source coverage. Use Node 24 and keep both coverage-final.json inputs when merging locally:
npm run test:run -- --selectProjects unit-node unit-dom --coverage --coverageDirectory=coverage/unit
npm run build
npm run test:run -- --selectProjects build types --coverage --coverageDirectory=coverage/build --maxWorkers=4
node scripts/merge-coverage.mjs coverage/unit/coverage-final.json coverage/build/coverage-final.jsonReal CLI child processes and browser execution provide behavioral assertions; their code execution is not automatically instrumented by the parent Jest process.