arrange-act-assert
Version:
The lightweight "Act-Arrange-Assert" oriented testing framework
647 lines (646 loc) • 22.5 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isMessage = isMessage;
exports.newRoot = newRoot;
const Util = __importStar(require("util"));
const Path = __importStar(require("path"));
const Fs = __importStar(require("fs"));
const V8 = __importStar(require("v8"));
const Assert = __importStar(require("assert"));
const Crypto = __importStar(require("crypto"));
const functionRunner_1 = require("./functionRunner");
const utils_1 = require("../utils/utils");
const default_1 = require("../formatters/default");
const spawnTestFile_1 = require("../spawnTestFile/spawnTestFile");
const singleton_1 = __importDefault(require("../coverage/singleton"));
const VALID_NAME_REGEX = /[^\w\-. ]/g;
let ids = 0;
class Test {
constructor(_context, _options, data) {
this._context = _context;
this._options = _options;
this.data = data;
this._promise = (0, utils_1.resolvablePromise)();
this._pendingPromise = null;
this._tests = [];
this._pending = [];
this._finished = false;
this._ended = false;
this._afters = [];
this._afterTest = [];
this._testErrors = [];
this.id = ids++;
this._addAfter = (data, cb) => {
this._afters.unshift(() => cb(data));
return data;
};
}
async run() {
try {
if (this._options.coverage) {
await singleton_1.default.start();
}
try {
this._context.send({
id: this.id,
type: 3
});
if (typeof this.data === "object") {
await this._runTest(this.data);
}
else {
await this._runDescribe(this.data);
}
}
finally {
try {
await this._runAfters();
}
finally {
try {
await this.end();
}
finally {
await this._runAfterTests();
}
}
}
const firstTestError = this._testErrors[0];
if (firstTestError) {
throw firstTestError.error;
}
this._context.send({
id: this.id,
type: 4
});
this._promise.resolve();
}
catch (e) {
this._context.send({
id: this.id,
type: 4,
error: Util.format(e)
});
this._promise.reject(e);
}
await this._promise;
}
async end() {
this._finished = true;
await this._awaitSubtests();
this._ended = true;
}
_isDescribe() {
return typeof this.data !== "object";
}
_getAsserts() {
if (typeof this.data === "object" && this.data.ASSERTS) {
return Object.entries(this.data.ASSERTS);
}
return [];
}
_getSnapshots() {
if (typeof this.data === "object" && "SNAPSHOTS" in this.data && this.data.SNAPSHOTS) {
return Object.entries(this.data.SNAPSHOTS);
}
return [];
}
describe(description, cb) {
return this._add(description, cb);
}
test(description, testData) {
return this._add(description, testData);
}
after(cb) {
if (this._ended) {
cb();
}
else {
this._afterTest.push(cb);
}
}
_add(description, testData) {
const test = new Test(this._context, {
...this._options,
description: description,
descriptionPath: [...this._options.descriptionPath, description]
}, testData);
if (this._finished) {
test._promise.reject(new Error("This test is closed. Can't add new tests to it"));
}
else {
this._tests.push(test);
this._pending.push(test);
this._context.send({
id: test.id,
type: 2,
test: {
parentId: this.id,
description: description,
type: test._isDescribe() ? 1 : 0
}
});
if (!this._pendingPromise) {
this._runPending();
}
}
return test._promise;
}
async _runPending() {
this._pendingPromise = (0, utils_1.resolvablePromise)();
while (true) {
const test = this._pending.shift();
if (!test) {
break;
}
try {
await test.run();
}
catch (error) {
this._testErrors.push({ test, error });
}
}
this._pendingPromise.resolve();
this._pendingPromise = null;
try {
await this._runAfterTests();
}
catch (error) {
this._testErrors.push({ test: this, error });
}
}
async _awaitSubtests() {
await Promise.allSettled(this._tests.map(test => test._promise));
if (this._pendingPromise) {
await this._pendingPromise;
}
}
async _runDescribe(cb) {
const result = await (0, functionRunner_1.functionRunner)("describe", cb || null, [buildTestFunction(this), this._addAfter]);
if (result.run && !result.ok) {
throw result.error;
}
}
async _checkSnapshot(testData, description) {
const path = [...this._options.descriptionPath, description || ""];
const cleanPath = path.map(name => name.replace(VALID_NAME_REGEX, "_"));
const hash = Crypto.createHash("shake128", { outputLength: 4 }).update(path.join("·")).digest("hex");
const file = `${Path.join(this._options.snapshotsFolder, ...cleanPath).substring(0, 246)}.${hash}`;
if (this._options.reviewSnapshots) {
throw new Error(`Review snapshot: ${file}\nValue: ${Util.inspect(testData, false, Infinity, false)}`);
}
let fileData;
try {
fileData = !this._options.regenerateSnapshots && await this._context.readFile(file);
}
catch (e) { }
if (fileData) {
const snapshot = V8.deserialize(fileData);
if (snapshot.validated && !this._options.regenerateSnapshots) {
Assert.deepStrictEqual(testData, snapshot.data);
}
else if (this._options.confirmSnapshots) {
Assert.deepStrictEqual(testData, snapshot.data);
snapshot.validated = true;
await this._context.writeFile(file, V8.serialize(snapshot));
}
else {
if (snapshot.validated) {
snapshot.validated = false;
snapshot.data = testData;
await this._context.writeFile(file, V8.serialize(snapshot));
}
else {
try {
Assert.deepStrictEqual(testData, snapshot.data);
}
catch (e) {
snapshot.data = testData;
await this._context.writeFile(file, V8.serialize(snapshot));
}
}
throw new Error(`Confirm snapshot: ${file}\nValue: ${Util.inspect(testData, false, Infinity, false)}`);
}
}
else if (!this._options.confirmSnapshots) {
const snapshot = {
validated: false,
data: testData
};
await this._context.writeFile(file, V8.serialize(snapshot));
throw new Error(`Confirm snapshot: ${file}\nValue: ${Util.inspect(testData, false, Infinity, false)}`);
}
else {
throw new Error("No snapshot file found. First run without confirmation to validate the snapshots");
}
}
async _runAssert(cb, args, description) {
if (cb) {
const id = ids++;
this._context.send({
id: id,
type: 2,
test: {
parentId: this.id,
description: description || "",
type: 2
}
});
this._context.send({
id: id,
type: 3
});
const assertResult = await (0, functionRunner_1.functionRunner)("ASSERT", cb, args);
if (assertResult.run) {
if (!assertResult.ok) {
this._context.send({
id: id,
type: 4,
error: Util.format(assertResult.error)
});
throw assertResult.error;
}
this._context.send({
id: id,
type: 4
});
}
}
return (0, functionRunner_1.functionRunner)("ASSERT", null, []);
}
async _runSnapshot(cb, args, description) {
if (cb) {
const id = ids++;
this._context.send({
id: id,
type: 2,
test: {
parentId: this.id,
description: description || "",
type: 2
}
});
this._context.send({
id: id,
type: 3
});
const res = await (0, functionRunner_1.functionRunner)("SNAPSHOT", cb && (async () => {
const result = await cb(...args);
await this._checkSnapshot(result, description);
return result;
}), []);
if (res.run && !res.ok) {
this._context.send({
id: id,
type: 4,
error: Util.format(res.error)
});
}
else {
this._context.send({
id: id,
type: 4
});
}
return res;
}
return (0, functionRunner_1.functionRunner)("SNAPSHOT", null, []);
}
async _runTest(test) {
const arrangeResult = await (0, functionRunner_1.functionRunner)("ARRANGE", test.ARRANGE || null, [this._addAfter]);
if (arrangeResult.run && !arrangeResult.ok) {
throw arrangeResult.error;
}
const actResult = await (0, functionRunner_1.functionRunner)("ACT", "ACT" in test && test.ACT || null, [arrangeResult.data, this._addAfter]);
let actResultData;
let snapshotResult;
if (actResult.run) {
if (!actResult.ok) {
throw actResult.error;
}
actResultData = actResult.data;
}
else {
snapshotResult = await this._runSnapshot("SNAPSHOT" in test && test.SNAPSHOT || null, [arrangeResult.data, this._addAfter]);
if (snapshotResult.run && !snapshotResult.ok) {
throw snapshotResult.error;
}
actResultData = snapshotResult.data;
}
if (test.ASSERT) {
const assertResult = await this._runAssert(test.ASSERT, [actResultData, arrangeResult.data, this._addAfter]);
if (assertResult.run && !assertResult.ok) {
throw assertResult.error;
}
}
let assertError = null;
if (!snapshotResult || !snapshotResult.run) {
for (const [description, cb] of this._getSnapshots()) {
const snapshotResult = await this._runSnapshot(cb, [actResultData, arrangeResult.data, this._addAfter], description);
if (snapshotResult.run && !snapshotResult.ok && !assertError) {
assertError = snapshotResult;
}
}
}
for (const [description, cb] of this._getAsserts()) {
const assertResult = await this._runAssert(cb, [actResultData, arrangeResult.data, this._addAfter], description);
if (assertResult.run && !assertResult.ok && !assertError) {
assertError = assertResult;
}
}
if (assertError) {
throw assertError.error;
}
}
async _runAfters() {
let doneError = null;
for (const cb of this._afters.splice(0)) {
const afterResult = await (0, functionRunner_1.functionRunner)("AFTER", cb, []);
if (afterResult.run && !afterResult.ok && !doneError) {
doneError = afterResult;
}
}
if (doneError && doneError.run && !doneError.ok) {
throw doneError.error;
}
}
async _runAfterTests() {
let doneError = null;
for (const cb of this._afterTest.splice(0)) {
const afterResult = await (0, functionRunner_1.functionRunner)("AFTER TEST", cb, []);
if (afterResult.run && !afterResult.ok && !doneError) {
doneError = afterResult;
}
}
if (doneError && doneError.run && !doneError.ok) {
throw doneError.error;
}
}
}
class Root extends Test {
constructor(notifyParentProcess, options) {
super({
send: msg => this.processMessage("", msg),
readFile: file => Fs.promises.readFile(file),
async writeFile(path, data) {
await Fs.promises.mkdir(Path.dirname(path), { recursive: true });
await Fs.promises.writeFile(path, data);
}
}, {
descriptionPath: [],
description: "",
snapshotsFolder: Path.join(process.cwd(), "snapshots"),
confirmSnapshots: false,
reviewSnapshots: false,
regenerateSnapshots: false,
coverage: false,
coverageExclude: [],
coverageNoBranches: false,
coverageNoSourceMaps: false,
...options
});
this.notifyParentProcess = notifyParentProcess;
this.formatter = null;
this.summary = {
test: {
count: 0,
ok: 0,
error: 0
},
assert: {
count: 0,
ok: 0,
error: 0
},
describe: {
count: 0,
ok: 0,
error: 0
},
total: {
count: 0,
ok: 0,
error: 0
},
failed: []
};
this._summaryMap = new Map();
}
processMessage(fileId, msg) {
if ("id" in msg && msg.id === this.id) {
return;
}
switch (msg.type) {
case 2: {
const id = `${fileId}_${msg.id}`;
switch (msg.test.type) {
case 0: {
this.summary.test.count++;
break;
}
case 1: {
this.summary.describe.count++;
break;
}
case 2: {
this.summary.assert.count++;
break;
}
}
this.summary.total.count++;
this._summaryMap.set(id, msg.test);
break;
}
case 4: {
const id = `${fileId}_${msg.id}`;
const test = this._summaryMap.get(id);
if (test) {
if (msg.error) {
switch (test.type) {
case 0: {
this.summary.test.error++;
break;
}
case 1: {
this.summary.describe.error++;
break;
}
case 2: {
this.summary.assert.error++;
break;
}
}
this.summary.total.error++;
this.summary.failed.push({ fileId: fileId, id: msg.id, test: test, error: msg.error });
}
else {
switch (test.type) {
case 0: {
this.summary.test.ok++;
break;
}
case 1: {
this.summary.describe.ok++;
break;
}
case 2: {
this.summary.assert.ok++;
break;
}
}
this.summary.total.ok++;
}
}
break;
}
}
if (this.formatter) {
this.formatter.format(fileId, msg);
}
else {
if (typeof process === "undefined") {
global.process = {};
}
if (this.notifyParentProcess) {
this.notifyParentProcess({
type: "testRunner",
data: msg
});
}
else {
if (!this.formatter) {
this.formatter = new default_1.DefaultFormatter();
}
this.formatter.format(fileId, msg);
}
}
}
setFormatter(formatter) {
this.formatter = formatter;
}
runTestFile(file, options) {
if (options.clearModuleCache) {
(0, utils_1.clearModuleCache)(file);
}
return require(file);
}
async spawnTestFile(file, options) {
this.processMessage(file, {
type: 0
});
try {
await (0, spawnTestFile_1.spawnTestFile)(file, options, msg => {
if (isMessage(msg)) {
this.processMessage(file, msg.data);
}
});
}
finally {
this.processMessage(file, {
type: 1
});
}
}
}
function isMessage(msg) {
return !!msg && typeof msg === "object" && "type" in msg && msg.type === "testRunner" && "data" in msg;
}
const testOptions = process.env.AAA_TEST_OPTIONS ? JSON.parse(process.env.AAA_TEST_OPTIONS) : (0, utils_1.getTestOptions)();
let root;
const files = new Set();
function addTestFiles() {
for (const file of (0, utils_1.getCallSites)()) {
files.add(file);
}
}
function getRoot() {
if (root) {
return root;
}
else {
const myRoot = newRoot(testOptions);
setImmediate(() => {
myRoot.run().catch((e) => {
process.exitCode = 1111;
if (!myRoot.formatter || !myRoot.formatter.formatSummary) {
console.error(e);
}
}).finally(async () => {
if (testOptions.coverage) {
myRoot.processMessage("", {
type: 5,
coverage: await singleton_1.default.takeCoverage()
});
}
if (myRoot.formatter && myRoot.formatter.formatSummary) {
myRoot.formatter.formatSummary(myRoot.summary, {
excludeFiles: Array.from(files),
exclude: testOptions.coverageExclude || [/\/node_modules\//i],
branches: !testOptions.coverageNoBranches,
sourceMaps: !testOptions.coverageNoSourceMaps
});
}
root = null;
});
});
return myRoot;
}
}
function buildTestFunction(myTest) {
function test(description, testData) {
addTestFiles();
return (myTest || getRoot()).test(description, testData);
}
test.test = test;
test.describe = function describe(description, cb) {
addTestFiles();
return (myTest || getRoot()).describe(description, cb);
};
test.after = function after(cb) {
addTestFiles();
return (myTest || getRoot()).after(cb);
};
return test;
}
function newRoot(options) {
addTestFiles();
const notifyParentProcess = process.env.AAA_TEST_FILE && process.send && process.send.bind(process) || null;
return root = new Root(notifyParentProcess, options);
}
exports.default = buildTestFunction(null);