let TEST = {};
const s_console = window.console;
window.console = {};
for (let [key, original] of Object.entries(s_console)) {
let intercept;
switch (key) {
case "assert":
intercept = function() {
if (arguments[0])
return;
TEST.log("CONSOLE ASSERT: " + Array.from(arguments).map(stringify).join(" "));
};
break;
case "trace":
intercept = function() {
TEST.log("CONSOLE TRACE: " + Array.from(arguments).map(stringify).join(" "));
try {
throw new Exception();
} catch (e) {
// Skip the first frame which is added by this function.
TEST.log(e.stack.split("\n").slice(1).join("\n"));
}
};
break;
default:
intercept = function() {
TEST.log(`CONSOLE ${key.toUpperCase()}: ` + Array.from(arguments).map(stringify).join(" "));
};
break;
}
window.console[key] = function() {
intercept.apply(this, arguments);
original.apply(this, arguments);
};
}
window.addEventListener("error", function handleError(event) {
TEST.fail(`unhandled error: ${event.message} (${event.filename}:${event.lineno}:${event.colno}\n`);
});
window.addEventListener("unhandledrejection", function handleUnhandledRejection(event) {
TEST.fail(`unhandled promise rejection: ${event.reason}\n`);
});
let s_instanceIdentifierMap = new Map;
function instanceIdentifier(object) {
let id = s_instanceIdentifierMap.get(object);
if (!id) {
id = s_instanceIdentifierMap.size + 1;
s_instanceIdentifierMap.set(object, id);
}
return id;
}
const defaultValueString = String(new Object); // [object Object]
function stringify(value) {
// Special case for numbers, since JSON.stringify converts Infinity and NaN to null.
if (typeof value === "number")
return value;
try {
return JSON.stringify(value);
} catch { }
try {
let valueString = String(value);
if (valueString === defaultValueString && value.constructor && value.constructor.name !== "Object")
return value.constructor.name + " instance " + instanceIdentifier(value);
return valueString;
} catch {
return defaultValueString;
}
}
const Expectation = {
True: Symbol("expect-true"),
False: Symbol("expect-false"),
Null: Symbol("expect-null"),
NotNull: Symbol("expect-not-null"),
Equal: Symbol("expect-equal"),
NotEqual: Symbol("expect-not-equal"),
LessThan: Symbol("expect-less-than"),
LessThanOrEqual: Symbol("expect-less-than-or-equal"),
GreaterThan: Symbol("expect-greater-than"),
GreaterThanOrEqual: Symbol("expect-greater-than-or-equal"),
};
function expect(type, condition, actual, {message, expected, values}) {
if (!message || !condition) {
actual = stringify(actual);
expected = stringify(expected);
if (!message) {
switch (type) {
case Expectation.True:
message = `expectTrue(${actual})`;
break;
case Expectation.False:
message = `expectFalse(${actual})`;
break;
case Expectation.Null:
message = `expectNull(${actual})`;
break;
case Expectation.NotNull:
message = `expectNotNull(${actual})`;
break;
case Expectation.Equal:
message = `expectEqual(${actual}, ${expected})`;
break;
case Expectation.NotEqual:
message = `expectNotEqual(${actual}, ${expected})`;
break;
case Expectation.LessThan:
message = `expectLessThan(${actual}, ${expected})`;
break;
case Expectation.LessThanOrEqual:
message = `expectLessThanOrEqual(${actual}, ${expected})`;
break;
case Expectation.GreaterThan:
message = `expectGreaterThan(${actual}, ${expected})`;
break;
case Expectation.GreaterThanOrEqual:
message = `expectGreaterThanOrEqual(${actual}, ${expected})`;
break;
default:
s_console.error("Unknown Expectation type: " + type);
break;
}
}
}
values = values.map(stringify);
if (condition) {
TEST.pass(message, ...values);
return;
}
let expectedMessage;
switch (type) {
case Expectation.True:
expectedMessage = `truthy`;
break;
case Expectation.False:
expectedMessage = `falsey`;
break;
case Expectation.Null:
expectedMessage = `null`;
break;
case Expectation.NotNull:
expectedMessage = `not null`;
break;
case Expectation.LessThan:
expectedMessage = `less than ${expected}`;
break;
case Expectation.LessThanOrEqual:
expectedMessage = `less than or equal to ${expected}`;
break;
case Expectation.GreaterThan:
expectedMessage = `greater than ${expected}`;
break;
case Expectation.GreaterThanOrEqual:
expectedMessage = `greater than or equal to ${expected}`;
break;
default:
expectedMessage = expected;
break;
}
message += "\n Expected: " + expectedMessage;
message += "\n Actual: " + actual;
TEST.fail(message, ...values);
}
let s_lastCasePromise = Promise.resolve();
let s_logWrapperNode = null;
TEST.case = function(name, description, callback) {
switch (arguments.length) {
case 1:
callback = name;
name = description = undefined;
break;
case 2:
callback = description;
description = undefined;
break;
}
s_console.assert(typeof callback === "function", callback);
s_lastCasePromise = s_lastCasePromise.then(() => new Promise(async (resolve, reject) => {
s_logWrapperNode = document.body.appendChild(document.createElement("section"));
if (name) {
let nameElement = s_logWrapperNode.appendChild(document.createElement("h1"));
nameElement.appendChild(document.createElement("pre")).textContent = name;
}
if (description) {
let descriptionElement = s_logWrapperNode.appendChild(document.createElement("p"));
descriptionElement.appendChild(document.createElement("pre")).textContent = description;
}
if (!callback.length || callback[Symbol.toStringTag] === "AsyncFunction")
await callback();
else
await new Promise(callback);
s_logWrapperNode = null;
// autosize <iframe> if running in a subframe
let frameElement = window.frameElement;
if (frameElement)
frameElement.style.setProperty("height", document.body.getBoundingClientRect().height + "px");
resolve();
}));
};
TEST.log = function(message, ...values) {
let parent = s_logWrapperNode || document.body;
let pre = parent.appendChild(document.createElement("pre"));
pre.append(message, ...values);
};
TEST.newline = function() {
TEST.log("");
};
TEST.json = function(object, filter) {
s_console.assert(!filter || typeof filter === "function", filter);
TEST.log(JSON.stringify(object, filter || null, 4));
};
TEST.pass = function(message, ...values) {
let pass = document.createElement("span");
pass.className = "pass";
pass.textContent = "PASS: ";
TEST.log(pass, message, ...values);
};
TEST.fail = function(message, ...values) {
s_console.error(message, ...values);
let fail = document.createElement("span");
fail.className = "fail";
fail.textContent = "FAIL: ";
TEST.log(fail, message, ...values);
};
TEST.assert = function(condition, message, ...values) {
s_console.assert(condition, message, ...values);
if (condition)
return;
let assert = document.createElement("span");
assert.className = "assert";
assert.textContent = "ASSERT: ";
TEST.log(assert, message, ...values);
};
TEST.expectTrue = function(actual, message, ...values) {
expect(Expectation.True, !!actual, actual, {
message,
values,
});
};
TEST.expectFalse = function(actual, message, ...values) {
expect(Expectation.False, !actual, actual, {
message,
values,
});
};
TEST.expectNull = function(actual, message, ...values) {
expect(Expectation.Null, actual === null, actual, {
message,
values,
});
};
TEST.expectNotNull = function(actual, message, ...values) {
expect(Expectation.NotNull, actual !== null, actual, {
message,
values,
});
};
TEST.expectEqual = function(actual, expected, message, ...values) {
expect(Expectation.Equal, expected === actual, actual, {
message,
expected,
values,
});
};
TEST.expectNotEqual = function(actual, expected, message, ...values) {
expect(Expectation.NotEqual, expected !== actual, actual, {
message,
expected,
values,
});
};
TEST.expectLessThan = function(actual, expected, message, ...values) {
expect(Expectation.LessThan, actual < expected, actual, {
message,
expected,
values,
});
};
TEST.expectLessThanOrEqual = function(actual, expected, message, ...values) {
expect(Expectation.LessThanOrEqual, actual <= expected, actual, {
message,
expected,
values,
});
};
TEST.expectGreaterThan = function(actual, expected, message, ...values) {
expect(Expectation.GreaterThan, actual > expected, actual, {
message,
expected,
values,
});
};
TEST.expectGreaterThanOrEqual = function(actual, expected, message, ...values) {
expect(Expectation.GreaterThanOrEqual, actual >= expected, actual, {
message,
expected,
values,
});
};
TEST.expectException = function(work) {
function expectAndDumpError(e) {
TEST.expectNotNull(e, "should produce exception");
if (!e)
return;
if (e instanceof Error || !(e instanceof Object)) {
TEST.log(e.toString());
return;
}
try {
TEST.json(e);
} catch {
TEST.log(e.constructor.name);
}
}
let caughtError = null;
let result = null;
try {
result = work();
} catch (e) {
caughtError = e;
} finally {
if (result instanceof Promise) {
return result.then((value) => {
expectAndDumpError(null);
return Promise.reject(value);
}, (reason) => {
expectAndDumpError(reason);
return Promise.resolve(reason);
});
}
expectAndDumpError(caughtError);
return caughtError ? Promise.resolve(caughtError) : Promise.reject(result);
}
};
export default TEST;