Engineer API reference

Small surface. Explicit state.

This reference documents the observable contract: exact exports, legal state transitions, return values, exceptions, console effects, event timing, immutable results, and host integration.

Module surface

Exports

import VanillaTest, {
    VanillaTest as NamedVanillaTest,
    VANILLA_TEST_COMPLETE_EVENT
} from 'vanilla-test';
ExportValueContract
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 stateLegal operationNext 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.
Core lifecycle

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 VanillaTest instance that is also an EventTarget.
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-type 2.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
false when test.strict is false.
Throws
TypeError with 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 true to throw on mismatches; assign false to return false.
Throws
TypeError when assigned a non-boolean.
Scope
Does not relax runner argument validation and is independent of pass(true) or fail(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')); // false
Inspect the example output

Browser: 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. description is a unique string for this instance.
Returns
A string shaped like "1) .expects addition works".
Throws
TypeError for a non-string. ReferenceError for 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. strict is a boolean.
Returns
The active numbered description.
Repeated call
Returns the existing description without changing state or logging. With this call's strict set to true, throws instead.
Throws
TypeError for a non-boolean. ReferenceError with no active test or after a strict repeated decision.
Console
Logs pass only 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. strict is a boolean.
Returns
The active numbered description.
Repeated call
Returns the existing description without changing state or logging. With this call's strict set to true, throws instead.
Throws
TypeError for a non-boolean. ReferenceError with no active test or after a strict repeated decision.
Console
Logs fail only 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
ReferenceError when no test is active.
Console
No output after an explicit decision. Logs fail when 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
ReferenceError when 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
listener is a function. The first subscription may occur before or after report(), provided completion has not already dispatched.
Returns
An idempotent zero-argument unsubscribe function.
Throws
TypeError for 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
iterations is a nonnegative safe integer.
Returns
The same runner instance, so test.delay(10).expects(...) is valid.
Throws
TypeError for 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); // true
Inspect the lifecycle output

Browser: 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.

  1. report() creates and freezes the snapshot.
  2. It logs the rendered report.
  3. 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.
  4. report() returns the snapshot before the listener runs.
  5. The event's detail is 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 ran
Inspect the event order

Browser: 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'
}
FieldValue shapeInvariant
passedFrozen array of numbered description stringsOne item for each first pass() decision.
failedFrozen array of numbered description stringsOne item for each first fail() decision, including automatic failures from done().
totalNonnegative integerpassed.length + failed.length.
failureCountNonnegative integerfailed.length.
okBooleantrue exactly when failureCount === 0.
reportStringThe rendered status, totals, failed list, and passed list with ANSI styling.
  • The result object, passed, and failed are frozen.
  • The first report result is cached by identity: test.report() === test.report().
  • report() never calls process.exit() and never assigns process.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 familyUseSuccess / 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.
Version 2 removes coercive edge cases.

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

MechanismUsed byImportant detail
typeofPrimitive helpers and objectobject(null) is true because JavaScript reports typeof null as 'object'.
instanceofArrays, dates, collections, promises, errors, typed arrays, buffers, Intl objects, and weak-reference objectsThe value must belong to the supplied host constructor.
Object.prototype.toStringGenerator values and async/function variantsThe helper compares the native symbol tag.
Exact identitycompare and compare-backed helperscompare(1, '1') is false in non-strict mode; strict mode throws.
Exact numeric predicatesfinite and NaNThey use Number.isFinite() and Number.isNaN() without coercion.
Alias behaviordefined, any, and existsAll three mean “not undefined.”

Common method catalogue

GroupMethods
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({}));               // false
Inspect the helper results

Browser: 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.

OperationConditionError
test.strict = valuevalue is not booleanTypeError
expects(description)description is not a stringTypeError
expects(description)Another test is activeReferenceError
expects(description)The raw description already ran on this instanceReferenceError
expects(description)The runner already reportedReferenceError
pass(strict) or fail(strict)strict is not booleanTypeError
pass() or fail()No test is activeReferenceError
pass(true) or fail(true)The active test already has either decisionReferenceError
done()No test is activeReferenceError
report()A test is still activeReferenceError
onComplete(listener)listener is not a functionTypeError
delay(iterations)Value is not a nonnegative safe integerTypeError
test.is.*(...)Value does not match while helper strict mode is enabledUsually TypeError; compare(), null(), globalThis(), and infinity() throw Error
Do not use strict mode as input coercion.

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();
}
This suite writes progress and its final 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.js
Check the terminal.

The 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>
Check the console or DevTools.

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

Experiment

Run code without setup

Edit a complete suite, execute it in an isolated browser frame, and inspect the mirrored output.

Open the playground →
Integrate

Use focused recipes

Start with passing, failing, asynchronous, event-driven, Node, and browser patterns.

Browse examples →