ATestRunner

A modern, flexible JavaScript test runner for the browser.

Tests

README

ATestRunner is a comprehensive suite for defining, running, and reporting tests which require a DOM environment. It operates on a queue-based system, allowing for asynchronous test execution with flexible output options to the console or a specified DOM element.

Installation

Download `ATestRunner.min.js` from the `src` directory and include it in an HTML page.

Quick Start

The default behavior is to log test results to the console.

Sample Test Suite

/******************** * The test suite * /tests/testobject.test.js *********************/ import obj from '../src/testObject.js'; import ATestRunner from './ATestrunner.js'; const runner = new ATestRunner(import.meta.url) const {equal, info, test, wait, when} = runner; // Helper function function insertElem(tag) { const elem = document.createElement(tag); document.body.append(elem); } info("Testing testObject") info("--- Basic Properties ---") test( "foo should be 'foo'", obj.foo, 'foo' ); test( 'bar should be null', obj.bar, null ); test( "baz should be undefined", obj.baz, undefined ); test( "arr should be an Array", Array.isArray(obj.arr), true ); test( "pojo.a should be 1", obj.pojo.a, 1 ); info("--- methods ---"); test( "getArr() should return [1,2,3]", equal(obj.getArr(), [1,2,3]), true ); test( "getPojo should return {a:1, b:2, c:3}", equal(obj.getPojo(), {a:1, b:2, c:3}), true ); test( "asyncFunc() should return 'foo'", async () => await obj.asyncFunc(), 'foo' ); test( "testing insertElem('pre') with when()", async () => { insertElem('pre'); await when(document.body.querySelector('pre')); const el = document.body.querySelector('pre'); el.remove(); return el instanceof HTMLPreElement; }, true ) test( "testing insertElem('pre') with wait()", async () => { insertElem('pre'); wait(10); const el = document.body.querySelector('pre'); el.remove(); return el instanceof HTMLPreElement; }, true ) runner.run();
/************************** * The object being tested * /src/testObj.js **************************/ export default { foo: 'foo', bar: null, baz: undefined, arr: [1,2,3], pojo: {a:1, b:2, c:3}, async asyncFunc(arg) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(arg ?? 'foo') }, 300); }); }, getArr() { return this.arr }, getPojo() { return this.pojo } };

API

test(gist, expression, expect)

This is the main function you will use to add your tests to the queue.

Example test ( "foo should be the same as foo", 'foo' === 'foo', true ) test ( "myObj.arr should equal [1, 2, 3]", equal(myObject.arr, [1, 2, 3]), true ); test ( "The expression can span multiple lines if it is enclosed in an anonymous function", () => { const foo = myObj.foo(); return foo; }, "foo" );

info( "..." )

info() simply prints some information.

info('Some helpful information');

equal( a, b )

Performs a deep equality comparison between two values. Handles primitives, objects, arrays, Dates, RegExps, Maps, Sets, and circular references.

Example const isEqual = equal(myArray, [1, 2, 3]) const isEqual = equal(myObject, { a:1, b:2, c:3 }) test( "myObject.foo() should return 'foo'", equal(myObject.foo(), 'foo'), true );

wait( ms )

Returns a promise that resolves after a specified number of milliseconds.

Example // pause execution for 50 milliseconds before continuing. await wait(50); test( "Waiting a few milliseconds for an HTML element to be ready", async () => { const div = document.createElement('div'); document.body.append(div); await wait(10); return document.body.querySelector('div') instanceof HTMLElement; }, true );

when( func, timeoutMs=1000, checkIntervalMs = 100 )

The when() method is a tool for testing asynchronous behaviors where you don't know exactly when a condition will be met. It's especially useful for situations involving animations, network requests, or complex state updates.

Examples // Waiting for a Simple Asynchronous State Change const state = { isDataLoaded: false }; function fetchData() { setTimeout(() => { state.isDataLoaded = true; }, 50); } fetchData(); test( "when() should wait for a state flag to become true", async () => { return await when(() => state.isDataLoaded); }, true );
// Waiting for a DOM Element to Appear function showPopup() { setTimeout(() => { const el = document.createElement('div'); el.id = 'popup-message'; el.textContent = 'Success!'; document.body.appendChild(el); }, 60); } showPopup(); test( "when() should wait for a DOM element to be created", async () => { const popupElement = await when( () => document.querySelector('#popup-message') ); return popupElement.textContent; }, 'Success!' ); // Waiting for a Spy's Call Count to Reach a Target const analytics = { ping: () => { // This method gets called by some other part of the app console.log('Ping!'); } }; function startPinging() { let count = 0; const intervalId = setInterval(() => { count++; analytics.ping(); if (count === 3) { clearInterval(intervalId); } }, 20); } // Spy on the method we want to track const pingSpy = spyOn(analytics, 'ping'); // Start the process that will call the spied method startPinging(); runner.test( "when() should wait for a spy to be called 3 times", async () => { await when(() => pingSpy.callCount === 3); }, true );

spyOn( obj, methodName )

Creates a spy on a method of an object. The original method is replaced with a spy that tracks calls and arguments, and then executes the original method.

Example const spy = spyOn(console, 'log'); console.log('hello'); // spy.callCount is 1 // spy.calls[0] is ['hello'] test( "spy callCount should be 1", spy.callCount, 1 ); test( "console.log should have been called with the argument 'hello'", spy.calls[0] === 'hello', true ); // Restore the original console.log method spy.restore();

*genCombos( options = {} )

A generator function that yields all possible combinations of properties from an options object. This is useful for data-driven or combinatorial testing.

Examples const options = { a: [1, 2], b: 'c' }; for (const combo of genCombos(options)) { // First iteration: combo is { a: 1, b: 'c' } // Second iteration: combo is { a: 2, b: 'c' } }

A More Detailed Example

Imagine you have a simple Button class that generates a CSS class string based on its properties.

class Button { constructor(options = {}) { this.color = options.color || 'secondary'; // 'primary', 'secondary', 'danger' this.size = options.size || 'medium'; // 'small', 'medium', 'large' this.disabled = options.disabled || false; // true or false this.text = options.text || 'Click Me'; } getClassName() { let classes = ['btn']; classes.push(`btn-${this.color}`); classes.push(`btn-${this.size}`); if (this.disabled) { classes.push('disabled'); } return classes.join(' '); } }

Instead of writing a separate test for every single button variation (primary small, primary medium, primary large, secondary small, etc.), we can use genCombos to generate all these variations for us and run them through a single test template.

// 1. Define all the possible options for our button. const buttonOptions = { color: ['primary', 'secondary', 'danger'], size: ['small', 'medium', 'large'], disabled: [true, false] }; // 2. Use a for...of loop to iterate through every combination generated. // This will run 3 (colors) * 3 (sizes) * 2 (disabled states) = 18 times. for (const combo of genCombos(buttonOptions)) { // 3. For each combination, create a dynamic test case. // The test name is generated from the combo, making failures easy to debug. const gist = `Button should render classes for color:${combo.color}, size:${combo.size}, disabled:${combo.disabled}`; // The test function creates a button with the current combo and gets its class name. const testFn = () => { const button = new Button(combo); return button.getClassName(); }; // The expected result is also generated dynamically based on the combo's properties. const expectedClasses = `btn btn-${combo.color} btn-${combo.size} ${combo.disabled ? ' disabled' : ''}`.trim(); // Add the fully formed, dynamic test to the queue. runner.test(gist, testFn, expectedClasses); } // Now, when you call runner.run(), it will execute all 18 generated tests.

Why this is so powerful

Scalability and Maintainability: Imagine you add a new size, 'xlarge'. Instead of writing several new tests, you only need to make one change:

// All you have to do is add 'xlarge' to the array. const buttonOptions = { color: ['primary', 'secondary', 'danger'], size: ['small', 'medium', 'large', 'xlarge'], disabled: [true, false] };

Your test suite instantly and automatically expands from 18 to 24 tests, covering all the new combinations without any extra effort.

benchmark( func, times = 1, thisArg = null, ...args)

Benchmarks a function by running it a specified number of times and measuring the total execution time. Works with both synchronous and asynchronous functions.

Examples const time = await benchmark( () => myHeavyFunction(), 100 ); console.log( `myHeavyFunction took ${time}ms to run 100 times.` );

Sending output to an HTML element

When output is set to a CSS selector (or an instance of HTMLElement), the test results are sent to the html element as a series of output elements containing JSON formatted strings.

/******************** * The test suite * /tests/testobject.test.js *********************/ import obj from '../src/testObject.js'; import ATestRunner from './ATestrunner.js'; const runner = new ATestRunner(import.meta.url) // Set output to a CSS selector runner.output = "#test-results"; // the tests ...