Skip to content

Instantly share code, notes, and snippets.

@isocroft
Last active September 5, 2024 14:50
Show Gist options
  • Save isocroft/4f8f23b4dbb1b1324a6706fa904fe5e9 to your computer and use it in GitHub Desktop.
Save isocroft/4f8f23b4dbb1b1324a6706fa904fe5e9 to your computer and use it in GitHub Desktop.
A toy assertion case processor for asserting on test assertion cases for a non-existent toy test framework (CommonJS) with the option to extend it
/* @HINT: NPM package for rendering colored text on the command-line (or standard output) */
const chalk = require('chalk');
/* @HINT: This uses the `chalk` NPM package to setup colors for failed test assertions as well as passed test assertions */
const failStatus = chalk.red;
const passStatus = chalk.green;
/* @HINT: A helper function using the status of an assertion to color text sent to the command-line (or standard output) */
const standardOutputPrettify = (isOk, statusText, prefix = "") => {
return isOk
? passStatus(chalk.white.bgGreen.bold(prefix) + ": " + statusText)
: failStatus(chalk.white.bgRed.bold(prefix) + ": " + statusText);
};
/* @HINT: A helper function that uses the case condition of an assertion to determine the status message for the assertion */
/**
* @NOTE:
*
* A case condition is used to decide how an assertion should be processed
*
* - an example is how modifiers like (.not, .andNot) which invert an assertion from a positive assertion to a negative one
*
*/
const getAssertionQualifier = (invert) => {
/**
* @INFO:
*
* If an assertion is inverted when a modifier like (.not) is used, then we qualify the status message differently
*/
return invert ? 'not to' : 'to';
};
/* @HINT: A helper function that turns a primitive or reference data type variable to a human-readable string */
const stringifyValue = (value) => {
if (typeof value !== 'object'
&& typeof value !== 'boolean') {
/* @INFO: Stringify primitive data types like: number, symbols e.t.c except booleans */
return String(value);
}
try {
if (value instanceof Error) {
throw new Error('do not JSON stringify');
}
/* @INFO: Stringify booleans and other reference ddata types likes: object literals, array e.t.c */
return JSON.stringify(value);
} catch (_) {
/* @INFO: Fallback: stringify error objects */
return String(value);
}
};
/* @HINT: Determine if a callback function is an `async` function */
const isAsyncFunction = (callback) => {
if (typeof callback !== 'function') {
return false
}
const $string = callback.toString().trim()
return Boolean(
$string.match(/^async /)
|| callback.constructor.name === 'AsyncFunction'
|| callback.__proto__.constructor.name === 'AsyncFunction'
)
};
/* @HINT: A helper function that determines if an object (or value) is a promise */
const isPromise = (object) => {
if (
typeof object === 'undefined' ||
object === null ||
!(object instanceof Object)
) {
return false;
}
return Boolean(
typeof object.then === 'function' ||
Object.prototype.toString.call(object) === '[object Promise]'
);
};
const isNotPromise = (object) => {
return !isPromise(object);
};
/* @HINT: A helper function used to create a pub/sub (mediator) object from listeing to and firing custom events */
function mitt(allEventsMap = {}) {
const all = Object.create(null);
return {
on(type, handler) {
allEventsMap[type] = -1;
(all[type] || (all[type] = [])).push(handler);
},
off(type, handler) {
if (typeof allEventsMap[type] !== 'undefined') {
delete allEventsMap[type];
}
if (all[type]) {
all[type].splice(all[type].indexOf(handler) >>> 0, 1);
}
},
emit(type) {
if (allEventsMap[type]) {
allEventsMap[type] = 1;
}
const _len = arguments.length;
const evts = new Array(_len > 1 ? _len - 1 : 0);
for (let _key = 1; _key < _len; _key++) {
evts[_key - 1] = arguments[_key];
}
(all[type] || []).slice().forEach((handler) => {
handler(...evts);
});
},
};
}
/**
* @INFO:
*
* Assertions can be inverted by certain modifiers (e.g. .not) in a test assertion case. However,
* each inverted assertion maps to one and only one case condition (or expectation).
*
* This is a constant that helps the codebase keep track of proper mapping of exactly one modifier to
* exactly one case condition (or expectation)
*/
const INVERTED_ASSERTION_SCAN_STATUS = {
ACTIVATED: 1,
DEACTIVATED: 2,
};
/** @NOTE:
*
* The `AssertionCaseProcessor` is a class that lays out the blueprint for how test assertion cases are processed.
*
* Each test assertion case maps to an instance of the `AssertionCaseProcessor` class.
*
* Every test assertion case (instance of the `AssertionCaseProcessor` class) is created with an `expect(...)` call.
*
* Every test assertion case has parts and those parts a called case conditions (or expectations)
*
* Every case condition can be inverted or not inverted depending on the qualifier placed in front of it
*
* - example:
* expect("yes").toBe("no") // This is a test assertion that has only one case condition (or expectation)
*
*
* expect("no").not.toBe("yes") // This is a test assertion that has only one case condition that is inverted with (.not)
*/
class AssertionCaseProcessor {
constructor(actualValue, $$emitter) {
/* @INFO: `actualValue` is the actual value to be asserted against */
this.actualValue = actualValue;
/* @INFO: This is the pub/sub instance used to send messages about every test assertion to the console (standard output) */
this.$$emitter = $$emitter;
/* @INFO: This is what helps the processor determine if the `actualValue` is a Promise or not */
this.shouldAwaitActualValue = false;
/* @INFO: This is what helps the processor determine if a case condition for a resolved/rejected promise was met */
this.awaitedActualValueExpectationMet = true;
/* @INFO: If only one modifier is used in a test assertion it can become globally applied to all case conditions */
this.invertAssertionGlobally = false;
/* @INFO: Keep a list of test assertion case modifiers for each case condition (or expectation) so it can be processed */
this.assertionCaseConditionModifiersList = [];
/* @INFO: Toggle for signaling when an assertion case moddifier has been consumed by its respective case condition */
this.invertedAssertionScanStatus =
INVERTED_ASSERTION_SCAN_STATUS.DEACTIVATED;
}
/**
* `toResolve`
*
* @example
* // returns "Promise<'{ "statusText": "verified \"0\" to be \"0\" after promise settled", "casePassed": true }'>"
* expect(Promise.resolve(0)).toResolve.toBe(0)
*/
get toResolve() {
if (isNotPromise(this.actualValue)) {
throw new Error(
"Cannot execute this test assertion case condition `.toResolve` against an object that isn't a promise"
)
return;
}
/* @INFO: If we get here then the `actualValue` is definitely a promise */
const $actualPromise = this.actualValue;
const caseConditionModifier = Boolean(this.assertionCaseConditionModifiersList.shift());
//const isAssertionCaseConditionInverted = caseConditionModifier === true;
this.invertedAssertionScanStatus =
INVERTED_ASSERTION_SCAN_STATUS.DEACTIVATED;
/* @INFO: Flag `actualValue` as a Promise object */
this.shouldAwaitActualValue = true;
/* @INFO: Override `actualValue` with new promise that merges the settlement for the intial promise (resolved or rejected)*/
this.actualValue = $actualPromise.then(
(value) => {
/* @HINT: If the assertion isn't inverted, then the case condition (or expectation) of a resolved promise is met */
/* @HINT: If the assertion is inverted, then the case condition (or expectation) of a resolved promise is not met */
this.awaitedActualValueExpectationMet = !caseConditionModifier;
return value;
},
(reason) => {
/* @HINT: If the assertion isn't inverted, then the case condition (or expectation) of a rejected promise is not met */
/* @HINT: If the assertion is inverted, then the case condition (or expectation) of a rejected promise is met */
this.awaitedActualValueExpectationMet = caseConditionModifier;
return reason;
}
);
return this;
}
/**
* `toReject`
*
* @example
* // returns "Promise<'{ "statusText": "expected leading case condition on promise to be met", "casePassed": false }'>"
* expect(Promise.resolve(0)).toReject.toBe(0)
*/
get toReject() {
if (isNotPromise(this.actualValue)) {
throw new Error(
"Cannot execute this test assertion case condition `.toReject` against an object that isn't a promise"
);
return;
}
/* @INFO: If we get here then the `actualValue` is definitely a promise */
const $actualPromise = this.actualValue;
const caseConditionModifier = Boolean(this.assertionCaseConditionModifiersList.shift());
//const isAssertionCaseConditionInverted = caseConditionModifier === true;
this.invertedAssertionScanStatus =
INVERTED_ASSERTION_SCAN_STATUS.DEACTIVATED;
/* @INFO: Flag `actualValue` as a Promise object */
this.shouldAwaitActualValue = true;
/* @INFO: Override `actualValue` with new promise that merges the settlement for the intial promise (resolved or rejected)*/
this.actualValue = $actualPromise.then(
(value) => {
/* @HINT: If the assertion isn't inverted, then the case condition (or expectation) of a resolved promise is not met */
/* @HINT: If the assertion is inverted, then the case condition (or expectation) of a resolved promise is met */
this.awaitedActualValueExpectationMet = caseConditionModifier;
return value;
},
(reason) => {
/* @HINT: If the assertion isn't inverted, then the case condition (or expectation) of a rejected promise is met */
/* @HINT: If the assertion is inverted, then the case condition (or expectation) of a rejected promise is not met */
this.awaitedActualValueExpectationMet = !caseConditionModifier;
return reason;
}
);
return this;
}
/**
* .not case condition modifier
*
* @example
* // returns "Promise<'{ "statusText": "expected \"0\" not to be \"0\" after promise settled", "casePassed": false }'>"
* expect(Promise.resolve(0)).not.toResolve.toBe(0)
*/
get not() {
if (this.assertionCaseConditionModifiersList.length > 0) {
throw new SyntaxError(
'Cannot use modifier `.not` in the middle of a test assertion'
);
}
if (
this.invertedAssertionScanStatus ===
INVERTED_ASSERTION_SCAN_STATUS.ACTIVATED
) {
throw new SyntaxError('Invalid test assertion case condition grammar');
}
this.invertedAssertionScanStatus = INVERTED_ASSERTION_SCAN_STATUS.ACTIVATED;
this.invertAssertionGlobally = true;
this.assertionCaseConditionModifiersList.push(this.invertAssertionGlobally);
return this;
}
/**
* .andNot case condition modifier
*
*
* @example
* // returns "Promise<'{ "statusText": "expected \"0\" not to be \"0\" after promise settled", "casePassed": false }'>"
* expect(Promise.resolve(0)).toResolve.andNot.toBe(0)
*/
get andNot() {
if (
!this.shouldAwaitActualValue &&
this.assertionCaseConditionModifiersList.length === 0
) {
throw new SyntaxError(
'Cannot use modifier `.andNot` at the start of a test assertion'
);
}
if (
this.invertedAssertionScanStatus ===
INVERTED_ASSERTION_SCAN_STATUS.ACTIVATED
) {
throw new SyntaxError('Invalid test assertion case condition grammar');
}
/* @INFO: If another assertion case condition is inverted then, any intial global inversion is reverted (set to "false") */
this.invertAssertionGlobally = false;
this.invertedAssertionScanStatus = INVERTED_ASSERTION_SCAN_STATUS.ACTIVATED;
this.assertionCaseConditionModifiersList.push(true);
return this;
}
/**
* .and case condition modifier
*
* @example
* // returns "Promise<'{ "statusText": "expected leading case condition on promise to be met", "casePassed": false }'>"
* expect(Promise.resolve(0)).not.toResolve.and.toBe(0)
*/
get and() {
if (
!this.shouldAwaitActualValue &&
this.assertionCaseConditionModifiersList.length === 0
) {
throw new SyntaxError(
'Cannot use modifier `.and` at the start of a test assertion'
);
}
if (
this.invertedAssertionScanStatus ===
INVERTED_ASSERTION_SCAN_STATUS.ACTIVATED
) {
throw new SyntaxError('Invalid test assertion case condition grammar');
}
this.invertedAssertionScanStatus = INVERTED_ASSERTION_SCAN_STATUS.ACTIVATED;
this.assertionCaseConditionModifiersList.push(false);
return this;
}
/**
* .toBe(...) assertion case matcher
*
* @param {*} expectedValue
*
* @example
* // return "Promise<'{ "statusText": "verified \"0\" not to be \"null\" after promise settled", "casePassed": true }'>"
* expect(Promise.resolve(0)).not.toResolve.toBe(null)
*/
toBe (expectedValue) {
const processExactCompareTestAssertionCase = (awaitedActualValue) => {
let result = false;
const currentCaseConditionModifier = this.assertionCaseConditionModifiersList.shift();
const invertAssertion =
currentCaseConditionModifier === undefined
? this.invertAssertionGlobally
: currentCaseConditionModifier;
this.invertedAssertionScanStatus =
INVERTED_ASSERTION_SCAN_STATUS.DEACTIVATED;
if (this.shouldAwaitActualValue) {
if (this.awaitedActualValueExpectationMet) {
result = invertAssertion
? !Object.is(expectedValue, awaitedActualValue)
: Object.is(expectedValue, awaitedActualValue);
} else {
if (
this.invertAssertionGlobally &&
currentCaseConditionModifier !== undefined
) {
result = false;
}
}
} else {
result = invertAssertion
? !Object.is(expectedValue, awaitedActualValue)
: Object.is(expectedValue, awaitedActualValue);
}
const isAssertionOk = result === true;
const qualifier = getAssertionQualifier(invertAssertion);
const finalTestAssertionState = {
statusText:
`${!isAssertionOk ? 'expected ' : 'verified '}` +
((this.shouldAwaitActualValue && !this.awaitedActualValueExpectationMet) &&
this.invertAssertionGlobally &&
currentCaseConditionModifier !== undefined
? `leading case condition ${this.shouldAwaitActualValue ? 'on promise' : ''} ${qualifier} be met`
: `"${stringifyValue(
awaitedActualValue
)}" ${qualifier} be "${stringifyValue(expectedValue)}"${this.shouldAwaitActualValue ? ' after promise settled' : ''}`),
casePassed: result,
};
/* @INFO: Emit event to dump test assertion message to command-line (or standard output) */
this.$$emitter.emit('stdout:dump', finalTestAssertionState);
return finalTestAssertionState;
};
if (this.shouldAwaitActualValue) {
return this.actualValue.then(processExactCompareTestAssertionCase);
}
return processExactCompareTestAssertionCase(this.actualValue);
}
/**
* .toHaveRaisedError(...) assertion case matcher
*
* @param {Error} error
*
* @example
* // returns '{ "statusText": "verified \"<Function>\" to have raised error > Error: hello", "casePassed": true }'
* expect(() => {
* throw new Error('hello')
* }).toHaveRaisedError(new Error('hello'))
*/
toHaveRaisedError(error) {
if (!error || !(error instanceof Error)) {
throw new Error('expected value is not an error');
}
if (typeof this.actualValue !== "function" || this.actualValue.length !== 0) {
throw new Error('actual callee not wrapped in caller function to track error')
}
if (isAsyncFunction(this.actualValue)) {
this.shouldAwaitActualValue = true
}
const processThrownErrorTestAssertionCase = (awaitedActualValue) => {
let result = false;
const currentCaseConditionModifier = this.assertionCaseConditionModifiersList.shift();
const invertAssertion =
currentCaseConditionModifier === undefined
? this.invertAssertionGlobally
: currentCaseConditionModifier;
this.invertedAssertionScanStatus =
INVERTED_ASSERTION_SCAN_STATUS.DEACTIVATED;
if (this.shouldAwaitActualValue) {
if (this.awaitedActualValueExpectationMet) {
result = invertAssertion
? !(error.name === awaitedActualValue.name && error.message === awaitedActualValue.message)
: error.name === awaitedActualValue.name && error.message === awaitedActualValue.message;
} else {
if (
this.invertAssertionGlobally &&
currentCaseConditionModifier !== undefined
) {
result = false;
}
}
} else {
result = invertAssertion
? !(error.name === awaitedActualValue.name && error.message === awaitedActualValue.message)
: error.name === awaitedActualValue.name && error.message === awaitedActualValue.message;
}
const isAssertionOk = result === true;
const qualifier = getAssertionQualifier(invertAssertion);
const finalTestAssertionState = {
statusText:
`${!isAssertionOk ? 'expected ' : 'verified '}` +
((this.shouldAwaitActualValue && !this.awaitedActualValueExpectationMet) &&
this.invertAssertionGlobally &&
currentCaseConditionModifier !== undefined
? `leading case condition ${this.shouldAwaitActualValue ? 'on promise' : ''} ${qualifier} be met`
: `"<Function>" ${qualifier} have raised error > ${stringifyValue(error)}${
this.shouldAwaitActualValue ? ' after promise settled' : ''
}`),
casePassed: result,
};
this.$$emitter.emit('stdout:dump', finalTestAssertionState);
return finalTestAssertionState;
};
if (this.shouldAwaitActualValue) {
return this.actualValue().then(() => {
this.awaitedActualValueExpectationMet = false;
return processThrownErrorTestAssertionCase({})
}).catch(processThrownErrorTestAssertionCase);
}
try {
this.actualValue();
return processThrownErrorTestAssertionCase({});
} catch ($e) {
return processThrownErrorTestAssertionCase($e);
}
}
/**
* .toBeThruthy(...) assertion case matcher
*
*
* @example
* // returns "Promise<'{ "statusText": "verified \"null\" not to be thruthy after promise settled", "casePassed": true }'>"
* expect(Promise.reject(null)).toReject.andNot.toBeTruthy()
*/
toBeTruthy() {
const processTruthyTestAssertionCase = (awaitedActualValue) => {
let result = false;
const currentCaseConditionModifier = this.assertionCaseConditionModifiersList.shift();
const invertAssertion =
currentCaseConditionModifier === undefined
? this.invertAssertionGlobally
: currentCaseConditionModifier;
this.invertedAssertionScanStatus =
INVERTED_ASSERTION_SCAN_STATUS.DEACTIVATED;
if (this.shouldAwaitActualValue) {
if (this.awaitedActualValueExpectationMet) {
result = invertAssertion
? !Boolean(awaitedActualValue)
: Boolean(awaitedActualValue);
} else {
if (
this.invertAssertionGlobally &&
currentCaseConditionModifier !== undefined
) {
result = false;
}
}
} else {
result = invertAssertion
? !Boolean(awaitedActualValue)
: Boolean(awaitedActualValue);
}
const isAssertionOk = result === true;
const qualifier = getAssertionQualifier(invertAssertion);
const finalTestAssertionState = {
statusText:
`${!isAssertionOk ? 'expected ' : 'verified '}` +
((this.shouldAwaitActualValue && !this.awaitedActualValueExpectationMet) &&
this.invertAssertionGlobally &&
currentCaseConditionModifier !== undefined
? `leading case condition ${this.shouldAwaitActualValue ? 'on promise' : ''} ${qualifier} be met`
: `"${stringifyValue(
awaitedActualValue
)}" ${qualifier} be thruthy${this.shouldAwaitActualValue ? ' after promise settled' : ''}`),
casePassed: result,
};
this.$$emitter.emit('stdout:dump', finalTestAssertionState);
return finalTestAssertionState;
};
if (this.shouldAwaitActualValue) {
return this.actualValue.then(processTruthyTestAssertionCase);
}
return processTruthyTestAssertionCase(this.actualValue);
}
}
/**
* Extend the assertion case processor with custom assertion case matchers
*
* @param {Object<string, Function>} assertionCasesMap
*
* @returns void
*
* @example
* // returns void
* expect.extendAssertionProcessorWith({
* toBeWithinNumberRange (
* qualifier,
* actualValue,
* rangeUpperBound = Number.MAX_SAFE_INTEGER,
* rangeLowerBound = Number.MIN_SAFE_INTEGER
* ) {
* if (typeof actualValue !== number
* && typeof rangeUpperBound !== "number"
* && typeof rangeLowerBound !== "number") {
* throw new Error('Invalid arguments for this test assertion case matcher')
* }
*
* const result = actualValue >= rangeLowerBound && actualValue <= rangeUpperBound;
* const isAssertionOk = result === true;
*
* return {
* statusText: `${!isAssertionOk ? 'expected' : 'verified'}
* "${stringifyValue()}" ${qualifier} be within number range: (${rangeUpperBound} - ${rangeLowerBound})
* ${this.shouldAwaitActualValue ? ' after promise settled' : ''}`,
* casePassed: result
* };
* }
* });
*/
expect.extendAssertionProcessorWith = (assertionCasesMap = {}) => {
const createAssertionCaseProcessorMethod = (assertionCaseFunction) =>
function () {
const currentCaseConditionModifier = this.assertionCaseConditionModifiersList.shift();
const invertAssertion =
currentCaseConditionModifier === undefined
? this.invertAssertionGlobally
: currentCaseConditionModifier;
const qualifier = getAssertionQualifier(invertAssertion);
const args = Array.from(arguments);
const processCustomTestAssertionCase = (awaitedActualValue) => {
const _finalTestAssertionState = assertionCaseFunction.apply(
this,
[qualifier, awaitedActualValue].concat(args)
);
this.$$emitter.emit('stdout:dump', _finalTestAssertionState);
return _finalTestAssertionState;
};
if (this.shouldAwaitActualValue) {
return this.actualValue.then(processCustomTestAssertionCase);
}
return processCustomTestAssertionCase(this.actualValue);
};
for (let assertionCaseName in assertionCasesMap) {
if (assertionCasesMap.hasOwnProperty(assertionCaseName)) {
const assertionCaseFunction = assertionCasesMap[assertionCaseName];
/* @INFO: JavaScript classes have a prototype that can be usedd to extend the class without syntactic sugar `extends` */
/* @CHECK: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain#constructors */
AssertionCaseProcessor.prototype[assertionCaseName] =
createAssertionCaseProcessorMethod(assertionCaseFunction);
}
}
};
/* @INFO: Setup pub/sub object to handle custom events from within the instance of `AssertionCaseProcessor` class */
expect.$$emitter = mitt();
/* @INFO: Setup hanlder for custom event `stdout:dump` to log to command-line (or standard output also called console) */
expect.$$emitter.on('stdout:dump', (state) => {
console.log(
standardOutputPrettify(
state.casePassed,
state.statusText,
state.casePassed ? "PASS" : "FAIL"
)
);
});
/* @INFO: The `expect(...)` function that creates test assertions */
function expect (actualValue) {
return new AssertionCaseProcessor(actualValue, expect.$$emitter);
}
module.exports = expect;
@isocroft
Copy link
Author

This is how to use it

const expect = require('./expect');

expect.extendAssertionProcessorWith({
  toBeNull(qualifier, actualValue) {
    const result = Object.is(actualValue, null);
    const isAssertionOk = result === true;

    return {
      statusText: `${
        !isAssertionOk ? 'expected ' : 'verified '
      }"${actualValue}" ${qualifier} be "null"`,
      casePassed: result,
    };
  },
});

expect(null).toBeNull();
expect(false).toBeTruthy();
expect(Promise.reject(0)).toReject.andNot.toBe(0);
Screenshot 2024-09-01 at 12 42 43 AM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment