@scintilla-network/litest
Version:
Dependency-free test framework with full Vitest API compatibility. Zero-dependency replacement for Vitest to reduce risk of supply chain attacks.
244 lines (218 loc) • 6.79 kB
JavaScript
/**
* @typedef {Object} TestSuite
* @property {string} name - The name of the test suite
* @property {Function[]} tests - Array of test functions
* @property {TestSuite[]} suites - Array of nested test suites
* @property {boolean} only - Whether this suite should be the only one to run
* @property {boolean} skip - Whether this suite should be skipped
* @property {Object} hooks - Lifecycle hooks for this suite
* @property {Function[]} hooks.beforeAll - Functions to run before all tests in this suite
* @property {Function[]} hooks.beforeEach - Functions to run before each test in this suite
* @property {Function[]} hooks.afterAll - Functions to run after all tests in this suite
* @property {Function[]} hooks.afterEach - Functions to run after each test in this suite
*/
/**
* @typedef {Object} TestContext
* @property {TestSuite[]} suites - Array of test suites
* @property {TestSuite|null} currentSuite - Currently active test suite
* @property {boolean} hasOnly - Whether any .only modifiers are present
*/
/** @type {TestContext} */
const testContext = {
suites: [],
currentSuite: null,
hasOnly: false
};
/**
* Creates a test suite
* @param {string} name - The name of the test suite
* @param {Function} fn - The function containing the tests
*/
function describe(name, fn) {
const suite = {
name,
tests: [],
suites: [],
only: false,
skip: false,
hooks: {
beforeAll: [],
beforeEach: [],
afterAll: [],
afterEach: []
}
};
const parentSuite = testContext.currentSuite;
if (parentSuite) {
parentSuite.suites.push(suite);
} else {
testContext.suites.push(suite);
}
testContext.currentSuite = suite;
fn();
testContext.currentSuite = parentSuite;
}
/**
* Creates a test suite that should be the only one to run
* @param {string} name - The name of the test suite
* @param {Function} fn - The function containing the tests
*/
describe.only = function(name, fn) {
testContext.hasOnly = true;
const suite = {
name,
tests: [],
suites: [],
only: true,
skip: false,
hooks: {
beforeAll: [],
beforeEach: [],
afterAll: [],
afterEach: []
}
};
const parentSuite = testContext.currentSuite;
if (parentSuite) {
parentSuite.suites.push(suite);
} else {
testContext.suites.push(suite);
}
testContext.currentSuite = suite;
fn();
testContext.currentSuite = parentSuite;
};
/**
* Creates a test suite that should be skipped
* @param {string} name - The name of the test suite
* @param {Function} fn - The function containing the tests
*/
describe.skip = function(name, fn) {
const suite = {
name,
tests: [],
suites: [],
only: false,
skip: true,
hooks: {
beforeAll: [],
beforeEach: [],
afterAll: [],
afterEach: []
}
};
const parentSuite = testContext.currentSuite;
if (parentSuite) {
parentSuite.suites.push(suite);
} else {
testContext.suites.push(suite);
}
testContext.currentSuite = suite;
fn();
testContext.currentSuite = parentSuite;
};
/**
* Creates a test suite that should skip if condition is true
* @param {boolean} condition - Condition to check
* @returns {Function} Describe function
*/
describe.skipIf = function(condition) {
return function(name, fn) {
if (condition) {
return describe.skip(name, fn);
} else {
return describe(name, fn);
}
};
};
/**
* Creates a test suite that should run only if condition is true
* @param {boolean} condition - Condition to check
* @returns {Function} Describe function
*/
describe.runIf = function(condition) {
return function(name, fn) {
if (!condition) {
return describe.skip(name, fn);
} else {
return describe(name, fn);
}
};
};
/**
* Creates a test suite that is a todo (not implemented yet)
* @param {string} name - The name of the test suite
* @param {Function} [fn] - Optional function containing the tests
*/
describe.todo = function(name, fn) {
console.log(`📝 TODO: ${name}`);
// Don't execute the suite, just log it
if (fn) {
// Create empty suite for structure
describe.skip(name, () => {});
}
};
/**
* Creates parameterized test suites using each data set
* @param {Array|Object} cases - Test cases data
* @returns {Function} Describe function
*/
describe.each = function(cases) {
return function(name, fn) {
if (Array.isArray(cases)) {
cases.forEach((testCase, index) => {
const suiteName = typeof name === 'function'
? name(...(Array.isArray(testCase) ? testCase : [testCase]))
: name.replace(/%[sdio%]/g, (match) => {
if (Array.isArray(testCase)) {
const value = testCase.shift();
return String(value);
}
return String(testCase);
});
describe(suiteName || `${name} [${index}]`, () => {
return Array.isArray(testCase) ? fn(...testCase) : fn(testCase);
});
});
} else if (typeof cases === 'object') {
Object.entries(cases).forEach(([key, value]) => {
const suiteName = name.replace('%s', key);
describe(suiteName, () => fn(value));
});
}
};
};
/**
* Creates parameterized test suites using for syntax
* @param {Array} cases - Test cases data
* @returns {Function} Describe function
*/
describe.for = function(cases) {
return function(name, fn) {
if (!Array.isArray(cases)) {
throw new Error('describe.for expects an array of test cases');
}
cases.forEach((testCase, index) => {
const suiteName = typeof name === 'function'
? name(testCase)
: name.replace(/%[sdio%]/g, () => String(testCase));
describe(suiteName || `${name} [${index}]`, () => fn(testCase));
});
};
};
/**
* Gets the current test context
* @returns {TestContext}
*/
function getTestContext() {
return testContext;
}
/**
* Resets the test context
*/
function resetTestContext() {
testContext.suites = [];
testContext.currentSuite = null;
testContext.hasOnly = false;
}
export { describe, getTestContext, resetTestContext };