refactor: Extract the rendering into an interface

This commit is contained in:
Jeremy Wall 2024-04-01 09:46:11 -04:00
parent 8841ca71bd
commit d4da1d7dee
3 changed files with 256 additions and 171 deletions

View File

@ -19,6 +19,32 @@
* @license Artistic-2.0 * @license Artistic-2.0
*/ */
/** @implements TapRenderer */
class NodeRenderer {
out(text) {
import('node:process').then(loaded => {;
loaded.stdout.write(text + "\n");
})
}
diag(lines) {
for (var line of lines) {
this.out('# ' + line);
}
}
}
/** @implements TapRenderer */
class BrowserRenderer {
out(text) {
// TODO(jeremy):
}
diag(lines) {
// TODO(jeremy):
}
}
/** /**
* The Tap Test Class helper. * The Tap Test Class helper.
*/ */
@ -31,35 +57,29 @@ class Tap {
passed = 0; passed = 0;
/** @type Number */ /** @type Number */
failed = 0; failed = 0;
/** @type function(TestOutput) */ /** @type TapRenderer */
#renderLine #renderer
/** /**
* Construct a new Tap Suite with a renderLine function. * Construct a new Tap Suite with a renderLine function.
* @param {function(string)} * @param {TapRenderer}
*/ */
constructor(renderFunc) { constructor(renderFunc) {
this.#renderLine = renderFunc; this.#renderer = renderFunc;
} }
static Browser() { static Browser() {
return new Tap(function(text) { return new Tap(new BrowserRenderer());
// TODO(zaphar): Handle output in a Browser context.
});
} }
static Node() { static Node() {
return new Tap(function(text) { return new Tap(new NodeRenderer());
import('node:process').then(loaded => {;
loaded.stdout.write(text + "\n");
})
});
} }
/** Renders output for the test results */ /** Renders output for the test results */
out(text) { out(text) {
this.#renderLine(text); this.#renderer.out(text);
}; };
/** /**
@ -78,18 +98,12 @@ class Tap {
}; };
/** Diagnostic formatter for TAP Output.
*
* @param msg {string}
*/
diag(msg){ diag(msg){
if (!msg) { if(!msg) {
msg = " "; msg = " ";
} }
var lines = msg.split("\n"); var lines = msg.split("\n");
for (var line of lines) { this.#renderer.diag(lines);
this.out('# ' + msg);
}
}; };
/** Render a pass TAP output message. /** Render a pass TAP output message.
@ -241,11 +255,8 @@ class Tap {
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') {
//this.diag('TypeOf ' + arguments[i] + ' method is: ' + typeof(obj[arguments[i]]) );
//this.diag('TypeOf prototype is: ' + typeof(obj.prototype) );
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]);
//this.diag('TypeOf prototype method is: ' + result);
if (result == 'undefined') { if (result == 'undefined') {
pass = false; pass = false;
this.diag('Missing ' + arguments[i] + ' method'); this.diag('Missing ' + arguments[i] + ' method');
@ -319,5 +330,59 @@ class Tap {
} }
/**
* Run a test and render a summarization.
*
* @param {Tap} t
* @param {string} testName
* @param {function(Tap)} test
*/
function runTest(t, testName, test) {
t.diag('running ' + testName + ' tests');
try {
test(t);
if (t.planned > t.counter) {
t.diag('looks like you planned ' + t.planned + ' tests but only ran '
+ t.counter + ' tests');
} else if (t.planned < t.counter) {
t.diag('looks like you planned ' + t.planned + ' tests but ran '
+ (t.counter - t.planned) + ' tests extra');
}
t.diag('ran ' + t.counter + ' tests out of ' + t.planned);
t.diag('passed ' + t.passed + ' tests out of ' + t.planned);
t.diag('failed ' + t.failed + ' tests out of ' + t.planned);
}
catch (err) {
t.diag("Test Suite Crashed!!! (" + err + ")");
}
return t;
}
/**
* Runs a set of TAP tests defined by a function.
* Uses the NodeRenderer for the test output.
*
* @param {string} testName
* @param {function(Tap)} test
*/
function runNodeTap(testName, test) {
var t = Tap.Node();
return runTest(t, testName, test);
}
/**
* Runs a set of TAP tests defined by a function.
* Uses the Browser renderer for the test output.
*
* @param {string} testName
* @param {function(Tap)} test
*/
function runBrowserTap(testName, test) {
var t = Tap.Browser();
return runTest(t, testName, test);
}
// TODO(zaphar): The runner interface as well. // TODO(zaphar): The runner interface as well.
export { Tap }; export { Tap, runNodeTap, runBrowserTap };

16
src/Tap.d.js Normal file
View File

@ -0,0 +1,16 @@
/** @interface */
class TapRenderer {
/** Renders output for the test results
* @param {string} text
*/
out(text) {
}
/** Diagnostic formatter for TAP Output.
*
* @param {Array<string>} lines
*/
diag(lines) {
}
}

View File

@ -1,150 +1,154 @@
import('../src/TAP.mjs').then(m => { import('../src/TAP.mjs').then(m => {
var out = "nothing yet"; var runNodeTap = m.runNodeTap;
var diag = "";
var t = m.Tap.Node(); function tapSuite(t) {
t.plan(27); var out = "nothing yet";
var diag = "";
var testCan = function () { t.plan(17);
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP thats failing
f.out = function(newout) { out = newout };
f.diag = function(newdiag) { diag += newdiag };
f.plan(4);
//mock a fake object to run test against var testCan = function () {
var obj = new Object; // setup fake test object
obj.run = function() {}; var f = new m.Tap(function(newout) { out = newout }); // the TAP thats failing
var method = 'run'; f.out = function(newout) { out = newout };
f.diag = function(newdiag) { diag += newdiag };
// begin real tests! f.plan(4);
f.can_ok(obj, 'not_there');
t.like(out, /not ok 1 - object can \[ not_there \]/, 'can_ok failed'); //mock a fake object to run test against
f.can_ok(obj, method); var obj = new Object;
diag = ''; obj.run = function() {};
t.like(out, /ok 2 - object can \[ run \]/, 'can_ok passed'); var method = 'run';
//Now we need to test the whole prototype method assignment thing // begin real tests!
f.can_ok(obj, 'not_there');
function MockObj() { t.like(out, /not ok 1 - object can \[ not_there \]/, 'can_ok failed');
this.attr = 1; f.can_ok(obj, method);
} diag = '';
t.like(out, /ok 2 - object can \[ run \]/, 'can_ok passed');
MockObj.prototype.fakeme = function () {};
//Now we need to test the whole prototype method assignment thing
f.can_ok(MockObj, 'fakeme');
diag = ''; function MockObj() {
t.like(out, /^ok .* \[ fakeme \]/, this.attr = 1;
'can_ok recognized prototype methods');
f.can_ok(MockObj, 'fakeme2');
diag = '';
t.like(out, /^not ok .* \[ fakeme2 \]/,
'can_ok prototype recognization doesnt break methods');
};
var testLike = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(1);
// begin real tests!
f.like("hello", /hello/, "hello matches hello");
t.like(out, /ok 1 - hello matches hello/, 'got description in TAP output');
};
var testDiag = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(10);
// begin real tests!
f.diag("hello");
t.like(out, /# hello/, 'got hello');
};
var testException = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(2);
// begin real tests!
f.throws_ok(function() {throw new Error('I made a boo boo')}, 'I made a boo boo');
//t.diag(out);
t.like(out, /ok 1 - code threw \[Error: I made a boo boo\]/, 'uncaught exception');
f.throws_ok(function() {}, 'I made a boo boo');
//t.diag(out);
t.like(out, /not ok 2 - code threw \[ \]/, 'false failed');
};
testException();
var testFails = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(3);
// begin real tests!
f.ok(false, 'false fails');
t.like(out, /not ok 1 - false fails/, 'false failed');
f.ok(0, 'zero fails');
t.like(out, /not ok 2 - zero fails/, '0 failed');
f.is(0, 1, 'zero is one');
t.like(out, /not ok 3 - zero is one/, '0 != 1');
};
testFails();
var testPass = function() {
t.ok(true, 'true is true');
t.is(1,1, '1 is 1');
t.pass('pass passes');
t.like("hello world", /hel+o/, 'regexen work');
t.unlike("hello there", /world/, 'no world');
};
testPass();
var testPlan = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(2);
// begin real tests!
f.ok(false, 'false fails');
t.is(f.counter, 1, 'counter increments by one');
t.is(f.planned, 2, 'planned = 2');
};
testPlan();
var testTodoSkip = function() {
var out;
t.can_ok(m.Tap, 'todo', 'skip');
var f = new m.Tap(); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(4);
f.todo(function() {
f.ok(true, 'true is true');
});
t.like(out, /ok 1 - # TODO: true is true/g,
'the non todo output is suitably formatted');
f.ok(!false, 'not false is true');
t.like(out, /ok 2 -/g, 'the regular output is suitably formatted');
f.skip(true, 'because I said so', 1,
function() {
f.is(1, 2, 'one is two');
} }
);
t.like(out, /^not ok 3 - # SKIP because I said so$/, MockObj.prototype.fakeme = function () {};
'the skipped output is suitably formatted');
f.is(1, 1, 'one is one'); f.can_ok(MockObj, 'fakeme');
t.like(out, /ok 4 - one is one/, diag = '';
'the non skipped output is suitable formatted'); t.like(out, /^ok .* \[ fakeme \]/,
}; 'can_ok recognized prototype methods');
testTodoSkip(); f.can_ok(MockObj, 'fakeme2');
diag = '';
return t; t.like(out, /^not ok .* \[ fakeme2 \]/,
'can_ok prototype recognization doesnt break methods');
};
var testLike = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(1);
// begin real tests!
f.like("hello", /hello/, "hello matches hello");
t.like(out, /ok 1 - hello matches hello/, 'got description in TAP output');
};
var testDiag = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(10);
// begin real tests!
f.diag("hello");
t.like(out, /# hello/, 'got hello');
};
var testException = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(2);
// begin real tests!
f.throws_ok(function() {throw new Error('I made a boo boo')}, 'I made a boo boo');
//t.diag(out);
t.like(out, /ok 1 - code threw \[Error: I made a boo boo\]/, 'uncaught exception');
f.throws_ok(function() {}, 'I made a boo boo');
//t.diag(out);
t.like(out, /not ok 2 - code threw \[ \]/, 'false failed');
};
testException();
var testFails = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(3);
// begin real tests!
f.ok(false, 'false fails');
t.like(out, /not ok 1 - false fails/, 'false failed');
f.ok(0, 'zero fails');
t.like(out, /not ok 2 - zero fails/, '0 failed');
f.is(0, 1, 'zero is one');
t.like(out, /not ok 3 - zero is one/, '0 != 1');
};
testFails();
var testPass = function() {
t.ok(true, 'true is true');
t.is(1,1, '1 is 1');
t.pass('pass passes');
t.like("hello world", /hel+o/, 'regexen work');
t.unlike("hello there", /world/, 'no world');
};
testPass();
var testPlan = function() {
// setup fake test object
var f = new m.Tap(function(newout) { out = newout }); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(2);
// begin real tests!
f.ok(false, 'false fails');
t.is(f.counter, 1, 'counter increments by one');
t.is(f.planned, 2, 'planned = 2');
};
testPlan();
var testTodoSkip = function() {
var out;
t.can_ok(m.Tap, 'todo', 'skip');
var f = new m.Tap(); // the TAP that's failing
f.out = function(newout) { out = newout };
f.plan(4);
f.todo(function() {
f.ok(true, 'true is true');
});
t.like(out, /ok 1 - # TODO: true is true/g,
'the non todo output is suitably formatted');
f.ok(!false, 'not false is true');
t.like(out, /ok 2 -/g, 'the regular output is suitably formatted');
f.skip(true, 'because I said so', 1,
function() {
f.is(1, 2, 'one is two');
}
);
t.like(out, /^not ok 3 - # SKIP because I said so$/,
'the skipped output is suitably formatted');
f.is(1, 1, 'one is one');
t.like(out, /ok 4 - one is one/,
'the non skipped output is suitable formatted');
};
testTodoSkip();
return t;
}
runNodeTap("Tap dogfood test suite", tapSuite);
}); });