UNPKG

arrange-act-assert

Version:

The lightweight "Act-Arrange-Assert" oriented testing framework

665 lines (664 loc) 22.8 kB
"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; function formatData(data) { const type = typeof data; let msg = `<${type}>\n`; if (type === "object") { msg += Util.inspect(data, { depth: Infinity, colors: false, maxArrayLength: Infinity, maxStringLength: Infinity }); } else { msg += data; } return msg; } 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.end(); } finally { try { await this._runAfters(); } 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}\n${formatData(testData)}`); } 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}\n${formatData(testData)}`); } } 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}\n${formatData(testData)}`); } 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) }); } else { this._context.send({ id: id, type: 4 }); } } return assertResult; } 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);