Module surface
Exports
import VanillaTest, {
VanillaTest as NamedVanillaTest,
VANILLA_TEST_COMPLETE_EVENT
} from 'vanilla-test';| Export | Value | Contract |
|---|---|---|
default |
VanillaTest class |
Exactly the same class object as the named export. |
VanillaTest |
Named class export | Extends the host's native EventTarget. |
VANILLA_TEST_COMPLETE_EVENT |
'vanilla-test:complete' |
The type used for the single asynchronous completion dispatch when a listener has been observed. |
The shared API depends only on Web-standard JavaScript. It does not import Node test APIs, inspect process, launch a browser, or terminate a host.
State model
Execution model
A runner has one active-test slot. Tests inside one instance are sequential; separate instances own separate state and can progress independently.
| Current state | Legal operation | Next state |
|---|---|---|
| Idle, unreported | expects(description) |
Active, undecided |
| Active, undecided | pass() or fail() |
Active, decided |
| Active, undecided or decided | done() |
Idle, unreported. An undecided test is failed first. |
| Idle, unreported | report() |
Reported and sealed |
| Reported and sealed | report() |
Unchanged; returns the cached snapshot. |
expects() → pass() | fail() → done(), repeated for each case, then exactly one effective report(). Calling done() without a decision is valid but records a failure.
First-decision-wins is unchanged. The active case carries its own decision marker, so pass(), fail(), and done() enforce that rule in constant time instead of searching the accumulated result arrays.
- Only one test can be active on an instance.
- Raw descriptions must be unique within an instance.
- A report seals the suite; no later test can start.
- An empty suite reports successfully with a total of zero.
- Instance state, strict mode, listeners, descriptions, and results are isolated.
Instance surface
Constructor and properties
new VanillaTest()
Creates an idle runner with no descriptions, decisions, listeners, or result snapshot.
- Arguments
- None.
- Returns
- A new
VanillaTestinstance that is also anEventTarget. - Throws
- Nothing from the constructor contract.
- Console
- No output.
test.is
Read-only access to the instance-owned strong-type helper. The same helper object is returned on every read.
- Value
- The complete isomorphic
strong-type2.0.1 instance, including 183 advertised shared validators plus core and extension methods. - Throws
- Nothing when read. Individual helper calls can throw on mismatches.
- Console
- No output.
test.compare
A direct reference to test.is.compare. It accepts (value, targetValue), uses exact identity via Object.is, returns true on a match, and otherwise throws a TypeError in strict mode or returns false.
- Identity
test.compare === test.is.compare.- Binding
- Call it as
test.compare(...); do not detach a method that relies on its receiver. - Console
- No output.
test.throw
A direct reference to test.is.throw. It accepts (valueType, expectedType) and is the mismatch path used by the type helper.
- Identity
test.throw === test.is.throw.- Returns
falsewhentest.strictisfalse.- Throws
TypeErrorwith expected and received type details when strict mode is enabled.- Console
- No output.
test.strict
A readable and writable boolean that controls type-helper mismatch behavior. It defaults to true.
- Set
- Assign
trueto throw on mismatches; assignfalseto returnfalse. - Throws
TypeErrorwhen assigned a non-boolean.- Scope
- Does not relax runner argument validation and is independent of
pass(true)orfail(true). - Console
- No output.
Strict helper behavior
const test = new VanillaTest();
test.compare(1, 1); // true: exact identity through Object.is
// test.compare(1, '1'); // would throw TypeError: not identical
test.strict = false;
console.log(test.is.number('42')); // false
console.log(test.compare(1, '1')); // falseBrowser: check the console or DevTools. Node: check the terminal.
Runner contract
Lifecycle methods
test.expects(description)
Starts the next test and writes its numbered description.
- Signature
expects(description)- Precondition
- The runner is unreported and has no active test.
descriptionis a unique string for this instance. - Returns
- A string shaped like
"1) .expects addition works". - Throws
TypeErrorfor a non-string.ReferenceErrorfor an active test, duplicate description, or sealed runner.- Console
- Logs the numbered description once.
test.pass(strict = false)
Stores the active test in the passed list and logs the decision.
- Signature
pass(strict = false)- Precondition
- A test is active.
strictis a boolean. - Returns
- The active numbered description.
- Repeated call
- Returns the existing description without changing state or logging. With this call's
strictset totrue, throws instead. - Throws
TypeErrorfor a non-boolean.ReferenceErrorwith no active test or after a strict repeated decision.- Console
- Logs
passonly for the first decision.
test.fail(strict = false)
Stores the active test in the failed list and logs the decision.
- Signature
fail(strict = false)- Precondition
- A test is active.
strictis a boolean. - Returns
- The active numbered description.
- Repeated call
- Returns the existing description without changing state or logging. With this call's
strictset totrue, throws instead. - Throws
TypeErrorfor a non-boolean.ReferenceErrorwith no active test or after a strict repeated decision.- Console
- Logs
failonly for the first decision.
test.done()
Closes the active test and clears the active slot. If no decision exists, it first records a failure.
- Signature
done()- Precondition
- A test is active.
- Returns
- The completed numbered description.
- Throws
ReferenceErrorwhen no test is active.- Console
- No output after an explicit decision. Logs
failwhen it auto-fails an undecided test.
test.report()
Builds the final result, freezes it, logs the rendered report, and seals the suite. Completion delivery is scheduled only when a completion listener has been observed.
- Signature
report()- Precondition
- No test is active.
- Returns
- A frozen result snapshot. Later calls return the exact same object.
- Throws
ReferenceErrorwhen a test is still active.- First call
- Logs once. It queues the one event microtask only when a completion listener is already registered; otherwise the first later completion subscription schedules it.
- Later calls
- No log and no new event.
test.onComplete(listener, options)
Subscribes a function to vanilla-test:complete. The optional second argument passes through to native addEventListener().
- Signature
onComplete(listener, options)- Precondition
listeneris a function. The first subscription may occur before or afterreport(), provided completion has not already dispatched.- Returns
- An idempotent zero-argument unsubscribe function.
- Throws
TypeErrorfor a non-function listener.- Console
- No output from subscription or unsubscription.
test.delay(iterations = 1000)
Performs a synchronous busy loop. This is a host-neutral compatibility primitive, not a timer and not a promise.
- Signature
delay(iterations = 1000)- Precondition
iterationsis a nonnegative safe integer.- Returns
- The same runner instance, so
test.delay(10).expects(...)is valid. - Throws
TypeErrorfor negative, fractional, infinite, unsafe, or non-number values.- Console
- No output.
One complete lifecycle
const test = new VanillaTest();
test.expects('addition preserves the total');
try {
test.compare(1 + 2, 3);
test.pass();
} catch (error) {
console.error(error);
test.fail();
} finally {
test.done();
}
const result = test.report();
console.log(result.ok); // trueBrowser: check the console or DevTools. Node: check the terminal.
EventTarget integration
Completion event timing
VanillaTest extends the native EventTarget. Its addEventListener() delegates to the native implementation and additionally notices completion subscriptions; removeEventListener() and dispatchEvent() retain their ordinary EventTarget behavior. Use onComplete() when you want the typed completion name plus an idempotent unsubscribe function.
report()creates and freezes the snapshot.- It logs the rendered report.
- If a completion listener has been observed, it queues a microtask that dispatches a native
CustomEvent. A runner that has never registered a completion listener queues no completion work. report()returns the snapshot before the listener runs.- The event's
detailis the exact returned snapshot object.
const test = new VanillaTest();
let phase = 'before report';
const unsubscribe = test.onComplete((event) => {
console.log(event.type); // vanilla-test:complete
console.log(event.detail.ok); // true
console.log(phase); // after report
}, { once: true });
test.expects('event delivery is asynchronous');
test.pass();
test.done();
const result = test.report();
phase = 'after report';
console.log(result.ok); // true, before event listener
await Promise.resolve(); // let the queued event run
unsubscribe(); // safe after a once-only listener ranBrowser: check the console or DevTools. Node: check the terminal.
Registering the first completion listener after report() schedules delivery from the frozen snapshot. Dispatch happens at most once, and there is no replay after it occurs. Repeated report() calls do not dispatch again.
Stable consumer boundary
Result contract
Use the structured fields for automation. The report string is human-facing ANSI output and should not be parsed.
{
passed: ['1) .expects addition preserves the total'],
failed: [],
total: 1,
failureCount: 0,
ok: true,
report: 'ANSI-rendered console report'
}| Field | Value shape | Invariant |
|---|---|---|
passed | Frozen array of numbered description strings | One item for each first pass() decision. |
failed | Frozen array of numbered description strings | One item for each first fail() decision, including automatic failures from done(). |
total | Nonnegative integer | passed.length + failed.length. |
failureCount | Nonnegative integer | failed.length. |
ok | Boolean | true exactly when failureCount === 0. |
report | String | The rendered status, totals, failed list, and passed list with ANSI styling. |
- The result object,
passed, andfailedare frozen. - The first report result is cached by identity:
test.report() === test.report(). report()never callsprocess.exit()and never assignsprocess.exitCode.- A host adapter decides how an unsuccessful result affects a shell, page, worker, or CI job.
strong-type delegation
strong-type 2.0.1 helper surface
test.is exposes the complete isomorphic helper with 183 advertised shared validators plus core and extension methods. Matching calls return true. Mismatches throw TypeError while test.strict is true, or return false while it is false. The families below are a practical map, not an exhaustive substitute for the searchable strong-type reference.
| Signature family | Use | Success / mismatch |
|---|---|---|
test.is.<predicate>(value) | Named primitive, value, function, error, iterator, collection, buffer, stream, platform, and capability predicates. | true / strict TypeError or non-strict false. |
test.is.typeCheck(value, type) | Compare typeof value with a type string. | true / helper mismatch behavior. |
test.is.instanceCheck(value, constructor) | Apply value instanceof constructor. | true / helper mismatch behavior. |
test.is.symbolStringCheck(value, tag) | Compare the native Object.prototype.toString tag. | true / helper mismatch behavior. |
test.is.union(value, typesString) | Try exact helper names from a pipe-delimited string such as 'string|number'. | true / helper mismatch behavior; an unknown helper name is also a mismatch. |
test.is.compare(value, targetValue) | Exact-identity comparison via Object.is; identical to test.compare. | true / strict TypeError or non-strict false. |
test.is.null(value), globalThis(value), infinity(value) | Exact checks for null, the host global object, and positive Infinity. | true / strict TypeError or non-strict false. |
test.is.throw(valueType, expectedType) | Invoke the helper mismatch path directly; identical to test.throw. | Strict TypeError or non-strict false. |
compare() uses Object.is, finite() uses Number.isFinite, and null() and infinity() require exact values. Values such as '1', undefined, and 'Infinity' are not coerced into matches.
How helpers decide
| Mechanism | Used by | Important detail |
|---|---|---|
typeof | Primitive helpers and object | object(null) is true because JavaScript reports typeof null as 'object'. |
instanceof | Arrays, dates, collections, promises, errors, typed arrays, buffers, Intl objects, and weak-reference objects | The value must belong to the supplied host constructor. |
Object.prototype.toString | Generator values and async/function variants | The helper compares the native symbol tag. |
| Exact identity | compare and compare-backed helpers | compare(1, '1') is false in non-strict mode; strict mode throws. |
| Exact numeric predicates | finite and NaN | They use Number.isFinite() and Number.isNaN() without coercion. |
| Alias behavior | defined, any, and exists | All three mean “not undefined.” |
Common method catalogue
| Group | Methods |
|---|---|
| Core | throw, typeCheck, instanceCheck, symbolStringCheck, compare |
| Presence and union | defined, any, exists, union |
| Special values | finite, NaN, null |
| Common values | array, boolean, bigInt, date, generator, asyncGenerator, globalThis, infinity, map, weakMap, number, object, promise, regExp, undefined, set, weakSet, string, symbol |
| Functions | function, asyncFunction, generatorFunction, asyncGeneratorFunction |
| Errors | error, evalError, rangeError, referenceError, syntaxError, typeError, URIError |
| Typed arrays | bigInt64Array, bigUint64Array, float32Array, float64Array, int8Array, int16Array, int32Array, uint8Array, uint8ClampedArray, uint16Array, uint32Array |
| Buffers and views | arrayBuffer, dataView, sharedArrayBuffer |
| Internationalization | intlDateTimeFormat, intlCollator, intlDisplayNames, intlListFormat, intlLocale, intlNumberFormat, intlPluralRules, intlRelativeTimeFormat |
| Weak references | finalizationRegistry, weakRef |
Predicates, unions, and comparisons
const test = new VanillaTest();
console.log(test.is.array([])); // true
console.log(test.is.date(new Date())); // true
console.log(test.is.union(42, 'string|number')); // true
console.log(test.compare(1, '1')); // throws in strict mode
test.strict = false;
console.log(test.is.promise({})); // falseBrowser: check the console or DevTools. Node: check the terminal.
union(value, typesString) splits a pipe-delimited list and calls helpers by their exact method names. Use names from the catalogue above, such as 'string|number|null'.
Failure modes
Error matrix
Runner contract errors are always enforced, even when test.strict is false.
| Operation | Condition | Error |
|---|---|---|
test.strict = value | value is not boolean | TypeError |
expects(description) | description is not a string | TypeError |
expects(description) | Another test is active | ReferenceError |
expects(description) | The raw description already ran on this instance | ReferenceError |
expects(description) | The runner already reported | ReferenceError |
pass(strict) or fail(strict) | strict is not boolean | TypeError |
pass() or fail() | No test is active | ReferenceError |
pass(true) or fail(true) | The active test already has either decision | ReferenceError |
done() | No test is active | ReferenceError |
report() | A test is still active | ReferenceError |
onComplete(listener) | listener is not a function | TypeError |
delay(iterations) | Value is not a nonnegative safe integer | TypeError |
test.is.*(...) | Value does not match while helper strict mode is enabled | Usually TypeError; compare(), null(), globalThis(), and infinity() throw Error |
test.strict = false changes only helper mismatch behavior. It does not make invalid lifecycle arguments, descriptions, listeners, or delays acceptable.
Complete integration
Runnable shared suite
Keep the test logic host-neutral and return the structured result. Each adapter decides how to expose success or failure.
test/shared-test.js
import VanillaTest from 'vanilla-test';
export default async function run() {
const test = new VanillaTest();
test.expects('addition preserves the total');
try {
test.compare(1 + 2, 3);
test.pass();
} catch (error) {
console.error(error);
test.fail();
} finally {
test.done();
}
test.expects('async values keep their shape');
try {
const value = await Promise.resolve({ ready: true });
test.is.object(value);
test.is.boolean(value.ready);
test.pass();
} catch (error) {
console.error(error);
test.fail();
} finally {
test.done();
}
return test.report();
}Browser: check the console or DevTools. Node: check the terminal.
Node adapter
Translate result.ok into a process exit code without changing the shared suite.
// test/node.js
import run from './shared-test.js';
const result = await run();
process.exitCode = result.ok ? 0 : 1;node ./test/node.jsThe runner prints each decision and the final report. The adapter exits with code 0 for success or 1 for failure.
Browser adapter
Map package specifiers to served ES modules, import the same suite, and render a small page-level status for non-console users.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="importmap">
{
"imports": {
"vanilla-test": "/node_modules/vanilla-test/index.js",
"ansi-colors-es6": "/node_modules/ansi-colors-es6/index.js",
"strong-type": "/node_modules/strong-type/index.js"
}
}
</script>
</head>
<body>
<p data-status>Running…</p>
<p><strong>Check the console or DevTools.</strong></p>
<script type="module">
import run from './shared-test.js';
const result = await run();
document.querySelector('[data-status]').textContent =
result.ok
? `Passed ${result.total} tests`
: `Failed ${result.failureCount} of ${result.total} tests`;
</script>
</body>
</html>Serve the page over HTTP. The browser console contains the detailed runner output; the page text contains the structured summary.
Apply the contract
Next steps
Run code without setup
Edit a complete suite, execute it in an isolated browser frame, and inspect the mirrored output.
Open the playground →Use focused recipes
Start with passing, failing, asynchronous, event-driven, Node, and browser patterns.
Browse examples →