import * as plugins from './smartexpect.plugins.js'; export type TExecutionType = 'sync' | 'async'; export class Assertion { executionMode: TExecutionType; baseReference: any; propertyDrillDown: Array = []; private notSetting = false; private timeoutSetting = 0; private failMessage: string; private successMessage: string; constructor(baseReferenceArg: any, executionModeArg: TExecutionType) { this.baseReference = baseReferenceArg; this.executionMode = executionModeArg; } private getObjectToTestReference() { let returnObjectToTestReference = this.baseReference; for (const property of this.propertyDrillDown) { if (returnObjectToTestReference == null) { // if it's null or undefined, stop break; } // We just directly access with bracket notation. // If property is a string, it's like obj["someProp"]; // If property is a number, it's like obj[0]. returnObjectToTestReference = returnObjectToTestReference[property]; } return returnObjectToTestReference; } public get not() { this.notSetting = true; return this; } public timeout(millisArg: number) { this.timeoutSetting = millisArg; return this; } public setFailMessage(failMessageArg: string) { this.failMessage = failMessageArg; return this; } public setSuccessMessage(successMessageArg: string) { this.successMessage = successMessageArg; return this; } private runCheck(checkFunction: () => any) { const runDirectOrNegated = (checkFunction: () => any) => { if (!this.notSetting) { return checkFunction(); } else { let isOk = false; try { runDirectOrNegated(checkFunction()); } catch (e) { isOk = true; } if (!isOk) { throw new Error(this.failMessage || 'Negated assertion is not ok!'); } } }; if (this.executionMode === 'async') { const done = plugins.smartpromise.defer(); if (!(this.baseReference instanceof Promise)) { done.reject(new Error(`${this.baseReference} is not of type promise.`)); } else { if (this.timeoutSetting) { plugins.smartdelay.delayFor(this.timeoutSetting).then(() => { if (done.status === 'pending') { done.reject(new Error(`${this.baseReference} timed out at ${this.timeoutSetting}!`)); } }); } this.baseReference.then((promiseResultArg: any) => { this.baseReference = promiseResultArg; done.resolve(runDirectOrNegated(checkFunction)); }); } return done.promise; } else { return runDirectOrNegated(checkFunction); } } public toBeDefined() { return this.runCheck(() => { if (this.getObjectToTestReference() === undefined) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not defined` ); } }); } public toBeTypeofString() { return this.runCheck(() => { if (typeof this.getObjectToTestReference() !== 'string') { throw new Error( this.failMessage || `Assertion failed: ${this.baseReference} with drill down ${ this.propertyDrillDown } is not of type string, but typeof ${typeof this.baseReference}` ); } }); } public toBeTypeofNumber() { return this.runCheck(() => { if (typeof this.getObjectToTestReference() !== 'number') { throw new Error( this.failMessage || `Assertion failed: ${this.baseReference} with drill down ${ this.propertyDrillDown } is not of type string, but typeof ${typeof this.baseReference}` ); } }); } public toBeTypeofBoolean() { return this.runCheck(() => { if (typeof this.getObjectToTestReference() !== 'boolean') { throw new Error( this.failMessage || `Assertion failed: ${this.baseReference} with drill down ${ this.propertyDrillDown } is not of type string, but typeof ${typeof this.baseReference}` ); } }); } public toEqual(comparisonObject: any) { return this.runCheck(() => { const result = plugins.fastDeepEqual(this.getObjectToTestReference(), comparisonObject); if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not equal ${comparisonObject}` ); } }); } public toMatch(comparisonObject: RegExp) { return this.runCheck(() => { const result = comparisonObject.test(this.getObjectToTestReference()); if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not match regex ${comparisonObject}` ); } }); } public toBeTrue() { return this.runCheck(() => { const result = typeof this.getObjectToTestReference() === 'boolean' && this.getObjectToTestReference() === true; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not true or not of type boolean` ); } }); } public toBeFalse() { return this.runCheck(() => { const result = typeof this.getObjectToTestReference() === 'boolean' && this.getObjectToTestReference() === false; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not false or not of type boolean` ); } }); } public toBeInstanceOf(classArg: any) { return this.runCheck(() => { const result = this.getObjectToTestReference() instanceof classArg; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not an instance of ${classArg}` ); } }); } public toHaveProperty(propertyArg: string, equalsArg?: any) { return this.runCheck(() => { const obj = this.getObjectToTestReference(); if (!obj || !(propertyArg in obj)) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not have property ${propertyArg}` ); } if (equalsArg !== undefined) { if (obj[propertyArg] !== equalsArg) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does have property ${propertyArg}, but it does not equal ${equalsArg}` ); } } }); } public toHaveDeepProperty(properties: string[]) { return this.runCheck(() => { let obj = this.getObjectToTestReference(); let currentPath = ''; for (const property of properties) { if (currentPath) { currentPath += `.${property}`; } else { currentPath = property; } if (!obj || !(property in obj)) { throw new Error( this.failMessage || `Missing property at path "${currentPath}" in ${this.baseReference}` ); } obj = obj[property]; } }); } public toBeGreaterThan(numberArg: number) { return this.runCheck(() => { const result = this.getObjectToTestReference() > numberArg; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not greater than ${numberArg}` ); } }); } public toBeLessThan(numberArg: number) { return this.runCheck(() => { const result = this.getObjectToTestReference() < numberArg; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not less than ${numberArg}` ); } }); } public toBeNull() { return this.runCheck(() => { const result = this.getObjectToTestReference() === null; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not null` ); } }); } public toBeUndefined() { return this.runCheck(() => { const result = this.getObjectToTestReference() === undefined; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not undefined` ); } }); } public toBeNullOrUndefined() { return this.runCheck(() => { const testRef = this.getObjectToTestReference(); const result = testRef === null || testRef === undefined; if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not null or undefined` ); } }); } // Array checks public toContain(itemArg: any) { return this.runCheck(() => { const testRef = this.getObjectToTestReference(); const result = Array.isArray(testRef) && testRef.includes(itemArg); if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} does not contain ${itemArg}` ); } }); } public toBeEmptyArray() { return this.runCheck(() => { const arrayRef = this.getObjectToTestReference(); if (!Array.isArray(arrayRef) || arrayRef.length !== 0) { throw new Error( this.failMessage || `Expected ${this.baseReference} to be an empty array, but it was not.` ); } }); } public toContainAll(values: any[]) { return this.runCheck(() => { const arrayRef = this.getObjectToTestReference(); if (!Array.isArray(arrayRef)) { throw new Error( this.failMessage || `Expected ${this.baseReference} with drill down ${ this.propertyDrillDown } to be an array.` ); } for (const value of values) { if (!arrayRef.includes(value)) { throw new Error( this.failMessage || `Expected ${this.baseReference} to include value "${value}", but it did not.` ); } } }); } public toExclude(value: any) { return this.runCheck(() => { const arrayRef = this.getObjectToTestReference(); if (!Array.isArray(arrayRef)) { throw new Error( this.failMessage || `Expected ${this.baseReference} with drill down ${ this.propertyDrillDown } to be an array.` ); } if (arrayRef.includes(value)) { throw new Error( this.failMessage || `Expected ${this.baseReference} to exclude value "${value}", but it included it.` ); } }); } public toStartWith(itemArg: any) { return this.runCheck(() => { const testObject = this.getObjectToTestReference(); const result = typeof testObject === 'string' && testObject.startsWith(itemArg); if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not start with ${itemArg}` ); } }); } public toEndWith(itemArg: any) { return this.runCheck(() => { const testObject = this.getObjectToTestReference(); const result = typeof testObject === 'string' && testObject.endsWith(itemArg); if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not end with ${itemArg}` ); } }); } public toBeOneOf(values: any[]) { return this.runCheck(() => { const result = values.includes(this.getObjectToTestReference()); if (!result) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not one of ${values}` ); } }); } public toHaveLength(length: number) { return this.runCheck(() => { const obj = this.getObjectToTestReference(); if (typeof obj.length !== 'number' || obj.length !== length) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not have a length of ${length}` ); } }); } public toBeCloseTo(value: number, precision = 2) { return this.runCheck(() => { const difference = Math.abs(this.getObjectToTestReference() - value); if (difference > Math.pow(10, -precision) / 2) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } is not close to ${value} up to ${precision} decimal places` ); } }); } public toThrow(expectedError?: any) { return this.runCheck(() => { let thrown = false; try { this.getObjectToTestReference()(); } catch (e) { thrown = true; if (expectedError && !(e instanceof expectedError)) { throw new Error( this.failMessage || `Expected function to throw ${expectedError.name}, but it threw ${e.name}` ); } } if (!thrown) { throw new Error(`Expected function to throw, but it didn't.`); } }); } public toBeTruthy() { return this.runCheck(() => { if (!this.getObjectToTestReference()) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not truthy` ); } }); } public toBeFalsy() { return this.runCheck(() => { if (this.getObjectToTestReference()) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not falsy` ); } }); } public toBeGreaterThanOrEqual(numberArg: number) { return this.runCheck(() => { if (this.getObjectToTestReference() < numberArg) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } is not greater than or equal to ${numberArg}` ); } }); } public toBeLessThanOrEqual(numberArg: number) { return this.runCheck(() => { if (this.getObjectToTestReference() > numberArg) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } is not less than or equal to ${numberArg}` ); } }); } public toMatchObject(objectArg: object) { return this.runCheck(() => { // Implement a partial object match if needed. const matchResult = plugins.fastDeepEqual(this.getObjectToTestReference(), objectArg); if (!matchResult) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not match the object ${JSON.stringify(objectArg)}` ); } }); } public toContainEqual(value: any) { return this.runCheck(() => { const arr = this.getObjectToTestReference(); if (!Array.isArray(arr)) { throw new Error( this.failMessage || `Expected ${this.baseReference} to be an array but it is not.` ); } const found = arr.some((item: any) => plugins.fastDeepEqual(item, value)); if (!found) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not contain the value ${JSON.stringify(value)}` ); } }); } public toBeArray() { return this.runCheck(() => { if (!Array.isArray(this.getObjectToTestReference())) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not an array` ); } }); } public toInclude(substring: string) { return this.runCheck(() => { const testRef = this.getObjectToTestReference(); if (typeof testRef !== 'string' || !testRef.includes(substring)) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not include the substring ${substring}` ); } }); } public toHaveLengthGreaterThan(length: number) { return this.runCheck(() => { const obj = this.getObjectToTestReference(); if (typeof obj.length !== 'number' || obj.length <= length) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not have a length greater than ${length}` ); } }); } public toHaveLengthLessThan(length: number) { return this.runCheck(() => { const obj = this.getObjectToTestReference(); if (typeof obj.length !== 'number' || obj.length >= length) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${ this.propertyDrillDown } does not have a length less than ${length}` ); } }); } public toBeDate() { return this.runCheck(() => { const testRef = this.getObjectToTestReference(); if (!(testRef instanceof Date)) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not a date` ); } }); } public toBeBeforeDate(date: Date) { return this.runCheck(() => { const testRef = this.getObjectToTestReference(); if (!(testRef instanceof Date) || testRef >= date) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not before ${date}` ); } }); } public toBeAfterDate(date: Date) { return this.runCheck(() => { const testRef = this.getObjectToTestReference(); if (!(testRef instanceof Date) || testRef <= date) { throw new Error( this.failMessage || `${this.baseReference} with drill down ${this.propertyDrillDown} is not after ${date}` ); } }); } public customAssertion( assertionFunction: (value: any) => boolean, errorMessage: string ) { return this.runCheck(() => { if (!assertionFunction(this.getObjectToTestReference())) { throw new Error(this.failMessage || errorMessage); } }); } /** * Drill into a property */ public property(propertyNameArg: string) { this.propertyDrillDown.push(propertyNameArg); return this; } /** * Drill into an array index */ public arrayItem(indexArg: number) { // Save the number (instead of "[index]") this.propertyDrillDown.push(indexArg); return this; } public log() { console.log(`this is the object to test:`); console.log(JSON.stringify(this.getObjectToTestReference(), null, 2)); return this; } }