UNPKG

saintest

Version:
583 lines (503 loc) 15.9 kB
/*! * saintest v0.2.0 | MIT License * Copyright (c) 2025-present NOuSantx */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.saintest = {})); })(this, (function (exports) { 'use strict'; const style = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', italic: '\x1b[3m', underline: '\x1b[4m', fg: (n) => `\x1b[38;5;${n}m`, bg: (n) => `\x1b[48;5;${n}m` }; const colors = { success: 35, error: 203, text: 245, highlight: 7, warning: 214, info: 39 }; function formatDiff(actual, expected) { return `\n ${style.fg(colors.success)}+ Expected: ${JSON.stringify(expected)}${style.reset} ${style.fg(colors.error)}- Received: ${JSON.stringify(actual)}${style.reset}` } function expect(actual) { return { toBe(expected) { if (actual !== expected) { throw new Error( `Expected ${JSON.stringify(actual)} to be ${JSON.stringify(expected)}${formatDiff( actual, expected )}` ) } }, toEqual(expected) { if (JSON.stringify(actual) !== JSON.stringify(expected)) { throw new Error(`Expected deep equality${formatDiff(actual, expected)}`) } }, toThrow(expectedError) { let threw = false; let thrownError = null; try { actual(); } catch (error) { threw = true; thrownError = error; } if (!threw) { throw new Error('Expected function to throw an error') } if (expectedError && thrownError.message !== expectedError) { throw new Error( `Expected error message "${expectedError}" but got "${thrownError.message}"` ) } }, toBeGreaterThan(expected) { if (!(actual > expected)) { throw new Error(`Expected ${actual} to be greater than ${expected}`) } }, toBeLessThan(expected) { if (!(actual < expected)) { throw new Error(`Expected ${actual} to be less than ${expected}`) } }, toContain(item) { if (!actual.includes(item)) { throw new Error(`Expected ${JSON.stringify(actual)} to contain ${JSON.stringify(item)}`) } }, toHaveLength(length) { if (actual.length !== length) { throw new Error(`Expected length of ${actual.length} to be ${length}`) } }, toBeInstanceOf(constructor) { if (!(actual instanceof constructor)) { throw new Error(`Expected ${actual} to be instance of ${constructor.name}`) } }, toBeTruthy() { if (!actual) { throw new Error(`Expected ${actual} to be truthy`) } }, toBeFalsy() { if (actual) { throw new Error(`Expected ${actual} to be falsy`) } }, toBeNull() { if (actual !== null) { throw new Error(`Expected ${actual} to be null`) } }, toBeUndefined() { if (actual !== undefined) { throw new Error(`Expected ${actual} to be undefined`) } }, toBeDefined() { if (actual === undefined) { throw new Error('Expected value to be defined') } }, toBeNaN() { if (!Number.isNaN(actual)) { throw new Error(`Expected ${actual} to be NaN`) } }, toMatch(regex) { if (!regex.test(actual)) { throw new Error(`Expected ${actual} to match ${regex}`) } }, not: { toBe(expected) { if (actual === expected) { throw new Error( `Expected ${JSON.stringify(actual)} not to be ${JSON.stringify(expected)}` ) } }, toEqual(expected) { if (JSON.stringify(actual) === JSON.stringify(expected)) { throw new Error(`Expected values not to be deeply equal${formatDiff(actual, expected)}`) } }, toBeInstanceOf(constructor) { if (actual instanceof constructor) { throw new Error(`Expected ${actual} not to be instance of ${constructor.name}`) } }, toMatch(regex) { if (regex.test(actual)) { throw new Error(`Expected ${actual} not to match ${regex}`) } }, toContain(item) { if (actual.includes(item)) { throw new Error( `Expected ${JSON.stringify(actual)} not to contain ${JSON.stringify(item)}` ) } }, toBeTruthy() { if (actual) { throw new Error(`Expected ${actual} not to be truthy`) } }, toBeFalsy() { if (!actual) { throw new Error(`Expected ${actual} not to be falsy`) } }, toBeNull() { if (actual === null) { throw new Error('Expected value not to be null') } }, toBeUndefined() { if (actual === undefined) { throw new Error('Expected value not to be undefined') } }, toBeDefined() { if (actual !== undefined) { throw new Error('Expected value to be undefined') } }, toBeNaN() { if (Number.isNaN(actual)) { throw new Error('Expected value not to be NaN') } }, toHaveLength(length) { if (actual.length === length) { throw new Error(`Expected length not to be ${length}`) } }, toBeGreaterThan(expected) { if (actual > expected) { throw new Error(`Expected ${actual} not to be greater than ${expected}`) } }, toBeLessThan(expected) { if (actual < expected) { throw new Error(`Expected ${actual} not to be less than ${expected}`) } }, toHaveProperty(propertyPath, value) { const properties = propertyPath.split('.'); let currentObject = actual; try { for (const property of properties) { currentObject = currentObject[property]; } if (value === undefined) { throw new Error(`Expected object not to have property "${propertyPath}"`) } if (currentObject === value) { throw new Error( `Expected property "${propertyPath}" not to have value ${JSON.stringify(value)}` ) } } catch (e) { // Property path doesn't exist, which is what we want for negative case return } }, toThrow(expectedError) { try { actual(); // If we get here, the function didn't throw, which is what we want } catch (error) { if (!expectedError) { throw new Error('Expected function not to throw an error') } if (error.message === expectedError) { throw new Error(`Expected function not to throw error "${expectedError}"`) } } }, toBeCloseTo(expected, precision = 2) { const multiplier = Math.pow(10, precision); const roundedActual = Math.round(actual * multiplier); const roundedExpected = Math.round(expected * multiplier); if (roundedActual === roundedExpected) { throw new Error( `Expected ${actual} not to be close to ${expected} with precision of ${precision} decimal points` ) } } }, toHaveProperty(propertyPath, value) { const properties = propertyPath.split('.'); let currentObject = actual; for (const property of properties) { if (!(property in currentObject)) { throw new Error(`Expected object to have property "${propertyPath}"`) } currentObject = currentObject[property]; } if (value !== undefined && currentObject !== value) { throw new Error( `Expected property "${propertyPath}" to have value ${JSON.stringify( value )}, got ${JSON.stringify(currentObject)}` ) } }, toBeCloseTo(expected, precision = 2) { const multiplier = Math.pow(10, precision); const roundedActual = Math.round(actual * multiplier); const roundedExpected = Math.round(expected * multiplier); if (roundedActual !== roundedExpected) { throw new Error( `Expected ${actual} to be close to ${expected} with precision of ${precision} decimal points` ) } } } } const testSuites = []; const defaultSuite = { name: 'Standalone Tests', tests: [], beforeEach: null, afterEach: null, beforeAll: null, afterAll: null }; exports.currentSuite = null; let passedTests = 0; let failedTests = 0; let skippedTests = 0; let startTime = 0; const getExecutionTime = () => { const endTime = performance.now(); return ((endTime - startTime) / 1000).toFixed(3) }; function describe(name, fn) { const suite = { name, tests: [], beforeEach: null, afterEach: null, beforeAll: null, afterAll: null }; testSuites.push(suite); const previousSuite = exports.currentSuite; exports.currentSuite = suite; fn(); exports.currentSuite = previousSuite; } function beforeEach(fn) { if (exports.currentSuite) { exports.currentSuite.beforeEach = fn; } else { defaultSuite.beforeEach = fn; } } function afterEach(fn) { if (exports.currentSuite) { exports.currentSuite.afterEach = fn; } else { defaultSuite.afterEach = fn; } } function beforeAll(fn) { if (exports.currentSuite) { exports.currentSuite.beforeAll = fn; } else { defaultSuite.beforeAll = fn; } } function afterAll(fn) { if (exports.currentSuite) { exports.currentSuite.afterAll = fn; } else { defaultSuite.afterAll = fn; } } function it(name, fn) { const test = { name, fn, skip: false, only: false, timeout: 100 }; if (exports.currentSuite) { exports.currentSuite.tests.push(test); } else { defaultSuite.tests.push(test); } return { skip: () => { test.skip = true; skippedTests++; }, only: () => { test.only = true; }, timeout: (ms) => { test.timeout = ms; } } } function test(name, fn) { return it(name, fn) } async function runTest(test, suite) { if (test.skip) { console.log( ` ${style.fg(colors.warning)}${style.reset} ` + `${style.fg(colors.text)}${test.name} ${style.italic}(skipped)${style.reset}` ); return { status: 'skipped' } } try { if (suite.beforeEach) await suite.beforeEach(); const testPromise = Promise.race([ Promise.resolve(test.fn()), new Promise((_, reject) => setTimeout(() => reject(new Error(`Test timed out after ${test.timeout}ms`)), test.timeout) ) ]); await testPromise; if (suite.afterEach) await suite.afterEach(); console.log( ` ${style.fg(colors.success)}${style.reset} ` + `${style.fg(colors.highlight)}${test.name}${style.reset}` ); return { status: 'passed' } } catch (error) { console.log( ` ${style.fg(colors.error)}${style.reset} ` + `${style.fg(colors.text)}${test.name}${style.reset}` ); console.log(`${style.fg(colors.text)} ${error.message}${style.reset}`); return { status: 'failed' } } } async function runSuite(suite) { if (suite.tests.length === 0) return let suitePassed = 0; let suiteFailed = 0; let suiteSkipped = 0; console.log( `\n${style.fg(colors.success)}${suite.name}${style.fg(colors.text)} ` + `[${suite.tests.length} tests]${style.reset}\n` ); try { if (suite.beforeAll) await suite.beforeAll(); const onlyTests = suite.tests.filter((t) => t.only); const testsToRun = onlyTests.length > 0 ? onlyTests : suite.tests; for (const test of testsToRun) { const result = await runTest(test, suite); switch (result.status) { case 'passed': suitePassed++; passedTests++; break case 'failed': suiteFailed++; failedTests++; break case 'skipped': suiteSkipped++; break } } if (suite.afterAll) await suite.afterAll(); const suiteTotal = suitePassed + suiteFailed + suiteSkipped; const suitePassedPercentage = ((suitePassed / (suiteTotal - suiteSkipped)) * 100).toFixed(2); console.log(`\n Suite Summary:`); console.log( ` ${style.fg(colors.success)}${suitePassedPercentage}%${style.reset} ` + `${style.fg(colors.text)}passing${style.reset}` ); console.log( ` ${style.fg(colors.success)}${suitePassed} passed${style.reset}` + ` · ` + `${style.fg(colors.error)}${suiteFailed} failed${style.reset}` + ` · ` + `${style.fg(colors.warning)}${suiteSkipped} skipped${style.reset}` + ` · ` + `${style.fg(colors.text)}${suiteTotal} total${style.reset}` ); } catch (error) { console.error(`${style.fg(colors.error)}Suite Error: ${error.message}${style.reset}`); } } async function run() { startTime = performance.now(); if (defaultSuite.tests.length > 0) { await runSuite(defaultSuite); } for (const suite of testSuites) { await runSuite(suite); } const executionTime = getExecutionTime(); const totalTestCount = passedTests + failedTests + skippedTests; const passedPercentage = ((passedTests / (totalTestCount - skippedTests)) * 100).toFixed(2); if (testSuites.length > 0 || defaultSuite.tests.length > 0) { console.log('\nFinal Test Results:'); console.log( `${style.fg(colors.success)}${passedPercentage}%${style.reset} ` + `${style.fg(colors.text)}of all tests passing${style.reset}` ); console.log( `${style.fg(colors.success)}${passedTests} passed${style.reset}` + ` · ` + `${style.fg(colors.error)}${failedTests} failed${style.reset}` + ` · ` + `${style.fg(colors.warning)}${skippedTests} skipped${style.reset}` + ` · ` + `${style.fg(colors.text)}${totalTestCount} total${style.reset}` ); console.log(`${style.fg(colors.text)}Total Time: ${executionTime}s${style.reset}\n`); } } var index = { expect, it, test, describe, beforeEach, afterEach, beforeAll, afterAll, run, testSuites, defaultSuite, currentSuite: exports.currentSuite }; exports.afterAll = afterAll; exports.afterEach = afterEach; exports.beforeAll = beforeAll; exports.beforeEach = beforeEach; exports.default = index; exports.defaultSuite = defaultSuite; exports.describe = describe; exports.expect = expect; exports.it = it; exports.run = run; exports.test = test; exports.testSuites = testSuites; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=index.umd.js.map