jest-test-each
Version:
run parametrised tests easily [typesafe] without text tables or arrays of arrays.
308 lines (307 loc) • 14.2 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TestEach = void 0;
const utils_1 = require("./utils/utils");
const tree_1 = require("./tree");
const name_1 = require("./utils/name");
const test_each_setup_1 = require("./test-each-setup");
const stripAnsi = require('strip-ansi');
class TestEach {
constructor(desc) {
var _a;
this.groups = [];
this.befores = [];
this.ensures = [];
this.ensuresCasesLength = [];
this.defects = [];
this.skippedTest = undefined;
this.onlyOne = false;
this.concurrentTests = false;
this.onlyOneFilter = undefined;
this.flatDescFunc = undefined;
this.testParentDesc = '';
this.entityName = (num, name) => {
return `${this.conf.numericCases ? num + '. ' : ''}${name}`;
};
this.testIfOnly = (testRunner) => {
if (this.onlyOne) {
this.runTest(testRunner, 'only() should be removed before committing', () => {
throw new Error('Do not forget to remove .only() from your test before committing');
});
}
};
this.findDefect = (testData) => {
const filterDefect = (data, defect) => {
const foundDefected = () => {
return defect && defect.filter ? defect.filter(data) : false;
};
const reasons = () => {
return defect.failReasons ? { actualFailReasonParts: defect.failReasons } : {};
};
const found = foundDefected();
const res = reasons();
return !defect.filter || found ? Object.assign({ defect: defect.reason }, res) : {};
};
let defect = {};
const defectObjs = this.defects.map(defect => filterDefect(testData, defect));
defectObjs.forEach(p => (defect = Object.assign(Object.assign({}, defect), p)));
return defect;
};
this.runIsDefectExist = (testRunner, allCases) => {
if (this.defects.length > 0) {
this.defects.forEach((defect, i) => {
const casesFound = allCases.filter(k => (defect.filter ? defect.filter(k.data) : true));
if (defect.filter && casesFound.length === 0) {
this.runTest(testRunner, 'Filtering defect returned no results ' + (i + 1), () => {
throw new Error('No such case: ' + defect.filter.toString());
});
return;
}
});
}
};
this.filterAndRunIfSearchFailed = (testRunner, allCases) => {
const casesFound = allCases.filter(k => this.onlyOneFilter ? this.onlyOneFilter(k.data) : false);
if (this.onlyOneFilter && casesFound.length === 0) {
this.runTest(testRunner, 'Only one search failed', () => {
throw new Error('No such case: ' + this.onlyOneFilter.toString());
});
}
return casesFound.length > 0 ? casesFound[0] : undefined;
};
this.runEnsures = (testRunner, allCases) => {
if (this.ensures.length > 0) {
this.ensures.forEach(ensure => {
this.runTest(testRunner, 'Ensure: ' + ensure.desc, () => {
ensure.check(allCases.map(p => p.data));
});
});
}
if (this.ensuresCasesLength.length > 0) {
this.ensuresCasesLength.forEach(ensure => {
this.runTest(testRunner, 'Ensure cases length', () => {
ensure(expect(allCases.map(p => p.data).length));
});
});
}
};
this.testParentDesc = desc;
this.conf = test_each_setup_1.testConfig.config;
this.env = (_a = test_each_setup_1.userEnv.env) !== null && _a !== void 0 ? _a : test_each_setup_1.testEnvDefault().env;
}
only(input) {
this.onlyOne = true;
if (input) {
this.onlyOneFilter = input;
}
return this;
}
config(config) {
this.conf = Object.assign(Object.assign({}, this.conf), config);
return this;
}
// todo proper
concurrent() {
this.concurrentTests = true;
return this;
}
// todo: should be ability to check previous levels
ensure(desc, cases) {
this.ensures.push({ desc, check: cases });
return this;
}
ensureCasesLength(exp) {
this.ensuresCasesLength.push(exp);
return this;
}
defect(reason, input, actualFailReasons) {
this.defects.push({ reason: reason, filter: input, failReasons: actualFailReasons });
return this;
}
skip(reason) {
this.skippedTest = reason;
return this;
}
desc(input) {
this.flatDescFunc = input;
return this;
}
before(before) {
this.befores.push(before);
return this;
}
each(cases) {
this.groups.push(cases);
return this;
}
runTest(testRunner, name, body, args, isBefore) {
const skipped = (args === null || args === void 0 ? void 0 : args.skip) || this.skippedTest;
const markedDefect = args === null || args === void 0 ? void 0 : args.defect;
const defectTestName = markedDefect ? ` - Marked with defect '${markedDefect}'` : '';
const testName = markedDefect ? name + defectTestName : name;
//? name.replace(/(, )?defect\:\s*('|"|`)[^'"`]*('|"|`)/, '') + defectTestName
const reasons = args === null || args === void 0 ? void 0 : args.actualFailReasonParts;
return testRunner(testName, () => __awaiter(this, void 0, void 0, function* () {
if (skipped) {
this.env.pending(`Test skipped: '${skipped}'`);
return;
}
if (markedDefect) {
// if it passes -> fail
// if it fails -> skip
let error = undefined;
yield this.runBody(body, args, isBefore)
.then(() => {
error = `Test doesn't fail but marked with defect`;
})
.catch(err => {
const alignedMessage = [stripAnsi(err.message), stripAnsi(err.stack)].join('\n');
// check reasons
if (!reasons || reasons.every(r => alignedMessage.includes(r))) {
// todo test will pass with jest-circus- need a way to skip test from inside for jest-circus
this.env.pending(`Test marked with defect '${markedDefect}': Actual fail reason:\\n ${alignedMessage}`);
}
if (reasons && !reasons.every(r => alignedMessage.includes(r))) {
throw new Error(`Actual fail reason doesn't contain [${reasons}]\nActual fail reason:\n "${alignedMessage}"`);
}
});
if (error) {
throw new Error(error);
}
}
else {
yield this.runBody(body, args, isBefore);
}
}));
}
runBody(body, args, isBefore) {
return __awaiter(this, void 0, void 0, function* () {
const beforeResults = [];
let beforeResult = {};
if (isBefore && this.befores.length > 0) {
for (const b of this.befores) {
const res = yield b(args || {});
if (res instanceof Object) {
beforeResults.push(res);
beforeResult = Object.assign(Object.assign({}, beforeResult), res);
}
}
}
try {
yield body(args || {}, beforeResult);
}
finally {
// after each
beforeResults.forEach((p) => (p.dispose ? p.dispose() : {}));
}
});
}
runCase(testRunner, body) {
return (t, i) => {
const name = this.entityName(i + 1, t.name.name);
this.runTest(testRunner, name, (args, b) => __awaiter(this, void 0, void 0, function* () {
const code = t.name.code;
if ((this.conf.testSuiteName.failOnReached && code === name_1.CODE_RENAME.nameTooLong) ||
code === name_1.CODE_RENAME.nameHasFunctions) {
utils_1.guard(!code, name_1.messageFromRenameCode(code, this.conf.testSuiteName.maxLength));
}
yield body(args, b);
}), t.data, true);
};
}
run(body) {
const { groupBySuites, testSuiteName, groupParentBySuite } = this.conf;
const maxTestNameLength = testSuiteName.maxLength;
const useConcurrency = this.concurrentTests || this.conf.concurrent;
const testRunner = this.onlyOne
? this.env.it.only
: useConcurrency
? this.env.it.concurrent
: this.env.it;
let allCases = [];
let casesErrors = [];
const root = tree_1.createTree(this.groups, maxTestNameLength, casesErrors, currentTest => {
var _a;
let defect = this.findDefect(Object.assign({}, currentTest.data));
const additionalData = Object.assign({}, defect);
const fullDatNoFlatDesc = [currentTest.data, additionalData];
const mergedFullData = utils_1.merge(fullDatNoFlatDesc);
const desc = mergedFullData.flatDesc || ((_a = this.flatDescFunc) === null || _a === void 0 ? void 0 : _a.call(this, mergedFullData));
const flatDesc = {
flatDesc: groupParentBySuite ? desc : desc ? `${this.testParentDesc} ${desc}` : undefined,
};
const fullData = [...fullDatNoFlatDesc, flatDesc.flatDesc ? flatDesc : []];
const partialData = [currentTest.partialData, additionalData];
const nameCaseFull = name_1.getName(fullData, maxTestNameLength);
const newName = name_1.getName(partialData, maxTestNameLength);
if (!groupBySuites && !groupParentBySuite) {
newName.name = `${this.testParentDesc} ${newName.name}`;
nameCaseFull.name = `${this.testParentDesc} ${nameCaseFull.name}`;
}
const testCase = Object.assign(Object.assign({}, currentTest), { data: utils_1.merge(fullData), name: newName, partialData: utils_1.merge(partialData) });
allCases.push(Object.assign(Object.assign({}, testCase), { name: nameCaseFull, isFlat: flatDesc.flatDesc }));
return testCase;
});
root.children.forEach(n => (n.name = groupParentBySuite ? n.name : `${this.testParentDesc} ${n.name}`));
root.tests.forEach(n => (n.name.name = groupParentBySuite ? n.name.name : `${this.testParentDesc} ${n.name.name}`));
const isFlat = allCases.every(p => p.isFlat);
if (this.onlyOne) {
const caseFound = this.filterAndRunIfSearchFailed(testRunner, allCases);
if (this.onlyOneFilter && caseFound === undefined) {
return;
}
allCases = this.onlyOneFilter ? [caseFound] : [allCases[0]];
}
if (this.groups.length === 0 && !!this.testParentDesc) {
// should not group into suite when only one case
this.runTest(testRunner, this.testParentDesc, body, undefined, true);
this.testIfOnly(testRunner);
return;
}
const suiteGuards = () => {
utils_1.guard(!(this.groups.length === 0 && !this.testParentDesc), 'Test should have name when no cases');
utils_1.guard(!this.groups.some(p => utils_1.checkObjEmpty(p)), 'Every case in .each should have not empty data');
utils_1.guard(!(allCases.length === 0), 'Test should have at least one test');
};
const tests = () => tree_1.treeWalk(root, (t, i, inside) => {
this.env.describe(this.entityName(i + 1, t.name), inside);
}, this.runCase(testRunner, body));
const testsFlat = () => allCases.forEach(this.runCase(testRunner, body));
runSuite(this.env.describe, () => {
suiteGuards();
if (casesErrors.length > 0) {
const defaultErr = 'test cases are empty';
casesErrors.forEach(casesError => {
this.runTest(testRunner, casesError.test || defaultErr, () => {
throw new Error([casesError.msg || defaultErr, casesError.details || ''].join('\n\n'));
});
});
}
if (!this.onlyOne) {
this.runEnsures(testRunner, allCases);
}
this.runIsDefectExist(testRunner, allCases);
this.testIfOnly(testRunner);
groupBySuites && !isFlat && !this.onlyOne ? tests() : testsFlat();
}, this.testParentDesc, groupParentBySuite);
}
}
exports.TestEach = TestEach;
const runSuite = (suiteRunner, callback, suiteName, groupParent) => {
if (suiteName && groupParent) {
suiteRunner(suiteName, () => {
callback();
});
return;
}
callback();
};