mirror of
https://github.com/zaphar/test-tap.git
synced 2025-07-21 20:19:49 -04:00
feat: Properly handle node async output
This commit is contained in:
parent
975d7f22af
commit
431b25cdfd
140
src/TAP.mjs
140
src/TAP.mjs
@ -21,10 +21,16 @@
|
|||||||
|
|
||||||
/** @implements TapRenderer */
|
/** @implements TapRenderer */
|
||||||
class NodeRenderer {
|
class NodeRenderer {
|
||||||
|
/** @type {Array<PromiseLike>} */
|
||||||
|
#thunks = [];
|
||||||
|
|
||||||
out(text) {
|
out(text) {
|
||||||
import('node:process').then(loaded => {;
|
this.#thunks.push(
|
||||||
loaded.stdout.write(text + "\n");
|
// Because this is a ECMAScript module we have to do dynamic module loads
|
||||||
})
|
// of the node ecosystem when running in Node.js.
|
||||||
|
import('node:process').then(loaded => {
|
||||||
|
loaded.stdout.write(text + "\n");
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
comment(lines) {
|
comment(lines) {
|
||||||
@ -32,13 +38,21 @@ class NodeRenderer {
|
|||||||
this.out('# ' + line);
|
this.out('# ' + line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This gives us a way to block on output. It's ghetto but async is a harsh task master.
|
||||||
|
async renderAll() {
|
||||||
|
for (var thunk of this.#thunks) {
|
||||||
|
await thunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @implements TapRenderer */
|
/** @implements TapRenderer */
|
||||||
class BrowserRenderer {
|
class BrowserRenderer {
|
||||||
#target = document.body;
|
#target = document.body;
|
||||||
|
|
||||||
/** @param {HtmlElement=} target */
|
/** @param {HtmlElement=} target */
|
||||||
constructor(target) {
|
constructor(target) {
|
||||||
if (target) {
|
if (target) {
|
||||||
this.#target = target;
|
this.#target = target;
|
||||||
@ -86,32 +100,44 @@ class Tap {
|
|||||||
/** @type Number */
|
/** @type Number */
|
||||||
counter = 0;
|
counter = 0;
|
||||||
/** @type Number */
|
/** @type Number */
|
||||||
passed = 0;
|
passed = 0;
|
||||||
/** @type Number */
|
/** @type Number */
|
||||||
failed = 0;
|
failed = 0;
|
||||||
/** @type TapRenderer */
|
/** @type TapRenderer */
|
||||||
#renderer
|
renderer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new Tap Suite with a renderLine function.
|
* Construct a new Tap Suite with a renderLine function.
|
||||||
* @param {TapRenderer}
|
* @param {TapRenderer}
|
||||||
*/
|
*/
|
||||||
constructor(renderer) {
|
constructor(renderer) {
|
||||||
this.#renderer = renderer;
|
this.renderer = renderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {{"Renderer": BrowserRenderer, "Tap": Tap}}
|
||||||
|
*/
|
||||||
static Browser() {
|
static Browser() {
|
||||||
|
var r = new BrowserRenderer();
|
||||||
|
return {"Renderer": r, "Tap": new Tap(r)};
|
||||||
return new Tap(new BrowserRenderer());
|
return new Tap(new BrowserRenderer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {{"Renderer": NodeRenderer, "Tap": Tap}}
|
||||||
|
*/
|
||||||
static Node() {
|
static Node() {
|
||||||
return new Tap(new NodeRenderer());
|
var r = new NodeRenderer();
|
||||||
|
return {"Renderer": r, "Tap": new Tap(r)};
|
||||||
|
}
|
||||||
|
|
||||||
|
isPass() {
|
||||||
|
return this.passed != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renders output for the test results */
|
/** Renders output for the test results */
|
||||||
out(text) {
|
out(text) {
|
||||||
this.#renderer.out(text);
|
this.renderer.out(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -120,8 +146,8 @@ class Tap {
|
|||||||
* @param {boolean} ok
|
* @param {boolean} ok
|
||||||
* @param {string=} description
|
* @param {string=} description
|
||||||
*/
|
*/
|
||||||
mk_tap(ok, description){
|
mk_tap(ok, description) {
|
||||||
if(!this.planned){
|
if (!this.planned) {
|
||||||
this.out("You tried to run tests without a plan. Gotta have a plan.");
|
this.out("You tried to run tests without a plan. Gotta have a plan.");
|
||||||
throw new Error("You tried to run tests without a plan. Gotta have a plan.");
|
throw new Error("You tried to run tests without a plan. Gotta have a plan.");
|
||||||
}
|
}
|
||||||
@ -130,12 +156,12 @@ class Tap {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
comment(msg){
|
comment(msg) {
|
||||||
if(!msg) {
|
if (!msg) {
|
||||||
msg = " ";
|
msg = " ";
|
||||||
}
|
}
|
||||||
var lines = msg.split("\n");
|
var lines = msg.split("\n");
|
||||||
this.#renderer.comment(lines);
|
this.renderer.comment(lines);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Render a pass TAP output message.
|
/** Render a pass TAP output message.
|
||||||
@ -143,9 +169,9 @@ class Tap {
|
|||||||
*/
|
*/
|
||||||
pass(description) {
|
pass(description) {
|
||||||
this.passed++;
|
this.passed++;
|
||||||
this.mk_tap('ok', description);
|
this.mk_tap('ok', description);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Render a fail TAP output message.
|
/** Render a fail TAP output message.
|
||||||
* @param {string} description
|
* @param {string} description
|
||||||
*/
|
*/
|
||||||
@ -153,7 +179,7 @@ class Tap {
|
|||||||
this.failed++;
|
this.failed++;
|
||||||
this.mk_tap('not ok', description);
|
this.mk_tap('not ok', description);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Run a function as a TODO test.
|
/** Run a function as a TODO test.
|
||||||
*
|
*
|
||||||
* @param {function(this:Tap, boolean, description)} func
|
* @param {function(this:Tap, boolean, description)} func
|
||||||
@ -162,12 +188,12 @@ class Tap {
|
|||||||
var self = this;
|
var self = this;
|
||||||
var tapper = self.mk_tap;
|
var tapper = self.mk_tap;
|
||||||
self.mk_tap = function(ok, desc) {
|
self.mk_tap = function(ok, desc) {
|
||||||
tapper.apply(self, [ok, "# TODO: "+desc]);
|
tapper.apply(self, [ok, "# TODO: " + desc]);
|
||||||
}
|
}
|
||||||
func();
|
func();
|
||||||
self.mk_tap = tapper;
|
self.mk_tap = tapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Run a function as a skip Test.
|
/** Run a function as a skip Test.
|
||||||
*
|
*
|
||||||
* @param {boolean} criteria
|
* @param {boolean} criteria
|
||||||
@ -182,8 +208,8 @@ class Tap {
|
|||||||
self.mk_tap = function(ok, desc) {
|
self.mk_tap = function(ok, desc) {
|
||||||
tapper.apply(self, [ok, desc]);
|
tapper.apply(self, [ok, desc]);
|
||||||
}
|
}
|
||||||
for(var i = 0; i < count; i++) {
|
for (var i = 0; i < count; i++) {
|
||||||
self.fail("# SKIP "+reason)
|
self.fail("# SKIP " + reason)
|
||||||
}
|
}
|
||||||
self.mk_tap = tapper;
|
self.mk_tap = tapper;
|
||||||
} else {
|
} else {
|
||||||
@ -200,7 +226,7 @@ class Tap {
|
|||||||
* @param {Number=} testCount
|
* @param {Number=} testCount
|
||||||
*/
|
*/
|
||||||
plan(testCount) {
|
plan(testCount) {
|
||||||
if(this.planned){
|
if (this.planned) {
|
||||||
throw new Error("you tried to set the plan twice!");
|
throw new Error("you tried to set the plan twice!");
|
||||||
}
|
}
|
||||||
if (!testCount) {
|
if (!testCount) {
|
||||||
@ -210,13 +236,13 @@ class Tap {
|
|||||||
this.out('1..' + testCount);
|
this.out('1..' + testCount);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#pass_if(func, desc){
|
#pass_if(func, desc) {
|
||||||
var result = func();
|
var result = func();
|
||||||
if(result) { this.pass(desc) }
|
if (result) { this.pass(desc) }
|
||||||
else { this.fail(desc) }
|
else { this.fail(desc) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// exception tests
|
// exception tests
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -229,16 +255,16 @@ class Tap {
|
|||||||
var errormsg = ' ';
|
var errormsg = ' ';
|
||||||
if (typeof func != 'function')
|
if (typeof func != 'function')
|
||||||
this.comment('throws_ok needs a function to run');
|
this.comment('throws_ok needs a function to run');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
catch(err) {
|
catch (err) {
|
||||||
errormsg = err+'';
|
errormsg = err + '';
|
||||||
}
|
}
|
||||||
this.like(errormsg, msg, 'code threw [' + errormsg + '] expected: [' + msg + ']');
|
this.like(errormsg, msg, 'code threw [' + errormsg + '] expected: [' + msg + ']');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that a function throws.
|
* Tests that a function throws.
|
||||||
*
|
*
|
||||||
@ -249,17 +275,17 @@ class Tap {
|
|||||||
var msg = false;
|
var msg = false;
|
||||||
if (typeof func != 'function')
|
if (typeof func != 'function')
|
||||||
this.comment('throws_ok needs a function to run');
|
this.comment('throws_ok needs a function to run');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
catch(err) {
|
catch (err) {
|
||||||
errormsg = err+'';
|
errormsg = err + '';
|
||||||
msg = true;
|
msg = true;
|
||||||
}
|
}
|
||||||
this.ok(msg, 'code died with [' + errormsg + ']');
|
this.ok(msg, 'code died with [' + errormsg + ']');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests that a function does not throw an exception.
|
* Tests that a function does not throw an exception.
|
||||||
*
|
*
|
||||||
@ -269,11 +295,11 @@ class Tap {
|
|||||||
var errormsg = true;
|
var errormsg = true;
|
||||||
if (typeof func != 'function')
|
if (typeof func != 'function')
|
||||||
this.comment('throws_ok needs a function to run');
|
this.comment('throws_ok needs a function to run');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
func();
|
func();
|
||||||
}
|
}
|
||||||
catch(err) {
|
catch (err) {
|
||||||
errormsg = false;
|
errormsg = false;
|
||||||
}
|
}
|
||||||
this.ok(errormsg, msg);
|
this.ok(errormsg, msg);
|
||||||
@ -287,9 +313,9 @@ class Tap {
|
|||||||
can_ok(obj) {
|
can_ok(obj) {
|
||||||
var desc = 'object can [';
|
var desc = 'object can [';
|
||||||
var pass = true;
|
var pass = true;
|
||||||
for (var i=1; i<arguments.length; i++) {
|
for (var i = 1; i < arguments.length; i++) {
|
||||||
if (typeof(obj[arguments[i]]) != 'function') {
|
if (typeof (obj[arguments[i]]) != 'function') {
|
||||||
if (typeof(obj.prototype) != 'undefined') {
|
if (typeof (obj.prototype) != 'undefined') {
|
||||||
var result = typeof eval('obj.prototype.' + arguments[i]);
|
var result = typeof eval('obj.prototype.' + arguments[i]);
|
||||||
if (result == 'undefined') {
|
if (result == 'undefined') {
|
||||||
pass = false;
|
pass = false;
|
||||||
@ -303,10 +329,10 @@ class Tap {
|
|||||||
desc += ' ' + arguments[i];
|
desc += ' ' + arguments[i];
|
||||||
}
|
}
|
||||||
desc += ' ]';
|
desc += ' ]';
|
||||||
this.#pass_if(function(){
|
this.#pass_if(function() {
|
||||||
return pass;
|
return pass;
|
||||||
}, desc);
|
}, desc);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -317,7 +343,7 @@ class Tap {
|
|||||||
* @param {string} desc
|
* @param {string} desc
|
||||||
*/
|
*/
|
||||||
is(got, expected, desc) {
|
is(got, expected, desc) {
|
||||||
this.#pass_if(function(){ return got == expected; }, desc);
|
this.#pass_if(function() { return got == expected; }, desc);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -328,7 +354,7 @@ class Tap {
|
|||||||
* @param {string} desc
|
* @param {string} desc
|
||||||
*/
|
*/
|
||||||
ok(got, desc) {
|
ok(got, desc) {
|
||||||
this.#pass_if(function(){ return !!got; }, desc);
|
this.#pass_if(function() { return !!got; }, desc);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -340,13 +366,13 @@ class Tap {
|
|||||||
* @param {string} desc
|
* @param {string} desc
|
||||||
*/
|
*/
|
||||||
like(string, regex, desc) {
|
like(string, regex, desc) {
|
||||||
this.#pass_if(function(){
|
this.#pass_if(function() {
|
||||||
if(regex instanceof RegExp) {
|
if (regex instanceof RegExp) {
|
||||||
return string.match(regex)
|
return string.match(regex)
|
||||||
} else {
|
} else {
|
||||||
return string.indexOf(regex) != -1
|
return string.indexOf(regex) != -1
|
||||||
}
|
}
|
||||||
}, desc)
|
}, desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -357,9 +383,9 @@ class Tap {
|
|||||||
* @param {string} desc
|
* @param {string} desc
|
||||||
*/
|
*/
|
||||||
unlike(string, regex, desc) {
|
unlike(string, regex, desc) {
|
||||||
this.#pass_if(function(){
|
this.#pass_if(function() {
|
||||||
return !string.match(regex);
|
return !string.match(regex);
|
||||||
}, desc)
|
}, desc)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,10 @@ class FakeRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import('./suite.mjs').then(m => {
|
import('./suite.mjs').then(async m => {
|
||||||
m.runTest(m.Tap.Node(), "Tap dogfood test suite", m.tapSuite);
|
const pair = m.Tap.Node();
|
||||||
|
m.runTest(pair.Tap, "Tap dogfood test suite", m.tapSuite);
|
||||||
|
// Note output requires some async machinery because it uses some dynamic inputs.
|
||||||
|
await pair.Renderer.renderAll();
|
||||||
|
process.exit(pair.Tap.isPass() ? 0 : 1);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/** @implements TapRenderer */
|
/** @implements TapRenderer */
|
||||||
import {Tap, runTest} from '../src/TAP.mjs';
|
import {Tap, runTest, NodeRenderer, BrowserRenderer} from '../src/TAP.mjs';
|
||||||
|
|
||||||
class FakeRenderer {
|
class FakeRenderer {
|
||||||
output = "nothing yet";
|
output = "nothing yet";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user