@exodus/test
Version:
A test suite runner
183 lines (162 loc) • 5.72 kB
JavaScript
let expect
let assertionsDelta = 0
const extend = []
const set = []
function fixupAssertions() {
if (assertionsDelta === 0) return
const state = expect.getState()
state.assertionCalls += assertionsDelta
state.numPassingAsserts += assertionsDelta
assertionsDelta = 0
}
function loadExpect(loadReason) {
if (expect) return expect
// eslint-disable-next-line no-undef
if (typeof EXODUS_TEST_LOAD_EXPECT !== 'undefined' && EXODUS_TEST_LOAD_EXPECT === false) {
if (loadReason === 'jest.mock') return // allow that and ignore if there is no usage
throw new Error('FATAL: expect() was optimized out')
} else {
try {
expect = require('expect').expect
} catch {
throw new Error(`Failed to load 'expect', required for ${loadReason}`)
}
// console.log('expect load reason:', loadReason)
for (const x of extend) expect.extend(...x)
for (const [key, value] of set) expect[key] = value
fixupAssertions()
return expect
}
}
const areNumeric = (...args) => args.every((a) => typeof a === 'number' || typeof a === 'bigint')
const matchers = {
__proto__: null,
toBe: (x, y) => Object.is(x, y),
toBeNull: (x) => x === null,
toBeTruthy: (x) => x,
toBeFalsy: (x) => !x,
toBeTrue: (x) => x === true,
toBeFalse: (x) => x === false,
toBeDefined: (x) => x !== undefined,
toBeUndefined: (x) => x === undefined,
toBeInstanceOf: (x, y) => y && x instanceof y,
toBeString: (x) => typeof x === 'string' || x instanceof String,
toBeNumber: (x) => typeof x === 'number', // yes, mismatches toBeString logic. yes, no bigints
toBeArray: (x) => Array.isArray(x),
toBeArrayOfSize: (x, l) => Array.isArray(x) && x.length === l,
toHaveLength: (x, l) => x && x.length === l,
toBeGreaterThan: (x, c) => areNumeric(x, c) && x > c,
toBeGreaterThanOrEqual: (x, c) => areNumeric(x, c) && x >= c,
toBeLessThan: (x, c) => areNumeric(x, c) && x < c,
toBeLessThanOrEqual: (x, c) => areNumeric(x, c) && x <= c,
toHaveBeenCalled: (x) => x?._isMockFunction && x?.mock?.calls?.length > 0,
toHaveBeenCalledTimes: (x, c) => x?._isMockFunction && x?.mock?.calls?.length === c,
toBeCalled: (...a) => matchers.toHaveBeenCalled(...a),
toBeCalledTimes: (...a) => matchers.toHaveBeenCalledTimes(...a),
toHaveBeenCalledOnce: (x) => matchers.toHaveBeenCalledTimes(x, 1),
}
const matchersFalseNegative = {
__proto__: null,
toEqual: (x, y) => Object.is(x, y),
toStrictEqual: (x, y) => Object.is(x, y),
toContain: (x, c) => Array.isArray(x) && [...x].includes(c),
toBeEven: (x) => Number.isSafeInteger(x) && x % 2 === 0,
toBeOdd: (x) => Number.isSafeInteger(x) && x % 2 === 1,
}
const doesNotThrow = (x) => {
try {
x()
return [true]
} catch (err) {
return [false, err]
}
}
const wrapAssertion = (f) => {
const wrapped = (...args) => {
if (!Error.captureStackTrace) return f(...args)
try {
return f(...args)
} catch (err) {
Error.captureStackTrace(err, wrapped)
throw err
}
}
return wrapped
}
function createExpect() {
return new Proxy(() => {}, {
apply: (target, that, [x, ...rest]) => {
if (rest.length > 0) return loadExpect('rest')(x, ...rest)
return new Proxy(Object.create(null), {
get: (_, name) => {
const matcher = matchers[name] || matchersFalseNegative[name]
if (matcher) {
return wrapAssertion((...args) => {
if (!matcher(x, ...args)) return loadExpect(`.${name} check`)(x)[name](...args)
assertionsDelta++
})
}
if (name === 'toThrow') {
return wrapAssertion((...args) => {
if (args.length > 0) return loadExpect('.toThrow args')(x)[name](...args)
const [passed] = doesNotThrow(x)
if (passed) return loadExpect('.toThrow fail')(() => {})[name](...args)
assertionsDelta++
})
}
if (name === 'not')
return new Proxy(Object.create(null), {
get: (_, not) => {
if (not === 'toThrow') {
return wrapAssertion((...args) => {
const [passed, err] = doesNotThrow(x)
if (!passed) {
return loadExpect('.not.toThrow fail')(() => {
throw err
}).not.toThrow(...args)
}
assertionsDelta++
})
}
if (matchers[not]) {
return wrapAssertion((...args) => {
if (matchers[not](x, ...args)) {
return loadExpect(`.not.${not} fail`)(x).not[not](...args)
}
assertionsDelta++
})
}
return loadExpect(`.not.${not}`)(x).not[not]
},
})
return loadExpect(`.${name}`)(x)[name]
},
})
},
get: (_, name) => {
if (name === 'extend' && !expect) return (...args) => extend.push(args)
if (name === 'extractExpectedAssertionsErrors') {
return expect
? (...args) => {
fixupAssertions()
return expect[name](...args)
}
: () => {
assertionsDelta = 0
return [] // no .assertions call were made, those cause loading
}
}
return loadExpect(`get ${name}`)[name]
},
set: (_, name, value) => {
if (expect) {
expect[name] = value
} else {
set.push([name, value])
}
return true
},
})
}
exports.expect = createExpect()
exports.loadExpect = loadExpect