import { fail } from 'jest'; function runAssertions(ctx, func) { try { const message = func() || ''; return { message: typeof message === 'function' ? message : () => message, pass: true, }; } catch (e) { return { pass: false, message: () => e.message || e, }; } } function assert(expr, failMessage) { if (!expr) { const finalMessage = typeof failMessage === 'function' ? failMessage() : failMessage; throw new Error(finalMessage); } } function prettyPrint(obj) { return JSON.stringify(obj, null, 2); } async function sleep(ms) { return new Promise(resolve => { window.setTimeout(() => { resolve(); }, ms); }); } function assertHasKeys(obj, keys, msg) { assert(obj, 'actual is not set'); assert(typeof obj === 'object', 'actual is not an object'); assert( (() => { const objectKeys = Object.keys(obj); return keys.reduce( (acc, cur) => acc && objectKeys.indexOf(cur) > -1, true ); })(), msg ); return msg; } function notFor(self) { return self.isNot ? ' not ' : ' '; } function testIsInstance(actual, ctor) { assert(actual !== undefined, 'actual is undefined'); assert(actual !== null, 'actual is null'); assert( actual instanceof ctor, `Expected instance of ${Object.prototype.toString.call( ctor )} but got ${actual}` ); } async function runAssertionsAsync(ctx, func) { try { await func(); return { message: () => '', pass: !ctx.isNot, }; } catch (e) { return { pass: false, message: () => e.message || e, }; } } beforeAll(() => { expect.extend({ toHaveKeys(actual, ...expected) { return runAssertions(this, () => { assert(expected, 'keys are not set'); const msg = () => `expected\n${prettyPrint(actual)}\nto have keys\n${prettyPrint( expected )}`; return assertHasKeys(actual, expected, msg); }); }, toHaveKey(actual, expected) { return runAssertions(this, () => { assert(expected, 'key is not set'); const msg = () => `expected ${prettyPrint(actual)} to have key "${expected}"`; return assertHasKeys(actual, [expected], msg); }); }, toBeEquivalentTo(actual, expected) { return runAssertions(this, () => { const msg = () => `expected collection equivalent to\n${prettyPrint( expected )}\nbut got\n${prettyPrint(actual)}`; assert(Array.isArray(actual), () => `${actual} is not an array`); assert(Array.isArray(expected), () => `${expected} is not an array`); assert(actual.length === expected.length, msg); assert( actual.reduce((acc, cur) => acc && expected.indexOf(cur) > -1, true), msg ); return msg; }); }, toBePrototypical(actual) { return runAssertions(this, () => { const msg = () => `expected${notFor(this)}prototype, but got ${prettyPrint(actual)}`; assert(actual, msg); assert(actual.prototype, msg); return msg; }); }, toBeAsyncFunction(actual) { return runAssertions(this, () => { const msg = () => `expected${notFor(this)}async function but got ${prettyPrint( actual )}`; assert( Object.prototype.toString.call(actual) === '[object AsyncFunction]' || Object.prototype.toString.call(actual) === '[object Function]', msg ); return msg; }); }, toBePromiseLike(actual) { return runAssertions(this, () => { const err = moreInfo => { return `expected something${notFor( this )}promise-like, but got ${actual}${ moreInfo ? '\n\t(' : '' }${moreInfo}${moreInfo ? ')' : ''}`; }; assert(actual, err); assert(typeof actual === 'object', err); assert( actual.then && typeof actual.then === 'function', 'should have a then function' ); return () => err(); }); }, toBeConstructor(actual) { return runAssertions(this, () => { const err = () => { return `expected ${actual}${notFor(this)}to be a constructor`; }; assert(actual, err); assert(actual.prototype, err); return err; }); }, toBeA(actual, ctor) { return runAssertions(this, () => { testIsInstance(actual, ctor); return () => `expected${notFor( this )}to get instance of ${ctor}, but received ${actual}`; }); }, toBeAn(actual, ctor) { return runAssertions(this, () => { testIsInstance(actual, ctor); return () => `expected${notFor( this )}to get instance of ${ctor}, but received ${actual}`; }); }, toBeVueComponent(actual, withName) { return runAssertions(this, () => { assert( typeof actual.render === 'function', `actual does not have a render function -- are you sure it's a Vue component?` ); assert( actual.name === withName, `Expected component${notFor( this )}to have name "${withName}", but found "${actual.name}"` ); return () => `Expected${notFor( this )}to receive a Vue component with name ${withName}`; }); }, toBeNumericInput(htmlElement) { return runAssertions(this, () => { const msg = () => `Expected${notFor(this)}to receive numeric input but got: ${ htmlElement.outerHTML }`; assert(htmlElement.type === 'number', msg); return msg; }); }, toHaveCssClass(actual, cssClass) { return runAssertions(this, () => { const msg = () => `Expected ${actual.outerHTML}${notFor( this )}to have css class "${cssClass}"`; const el = actual.$el || actual; assert(el.classList.contains(cssClass), msg); return msg; }); }, toHaveBeenCalledOnce(actual) { return runAssertions(this, () => { if (this.isNot) { throw new Error( [ "Negation of 'toHaveBeenCalledOnce' is ambiguous ", "(do you mean 'not at all' or 'any number except 1'?)", ].join('') ); } expect(actual).toHaveBeenCalledTimes(1); return () => `Expected${notFor(this)}to have been called once`; }); }, toHaveBeenCalledOnceWith(actual, ...args) { return runAssertions(this, () => { expect(actual).toHaveBeenCalledTimes(1); expect(actual).toHaveBeenCalledWith(...args); return () => `Expected${notFor(this)}to have been called once with ${args}`; }); }, toBeHidden(actual) { return runAssertions(this, () => { const msg = () => `Expected '${actual.outerHTML}'${notFor(this)}to be hidden`; assert(actual, 'actual does not exist'); assert(actual.style, 'actual may not be an html element?'); assert( actual.style.display === 'none' || actual.style.visibility === 'hidden' || actual.style.visibility === 'collapse', msg ); return msg; }); }, toBeVisible(htmlElement) { return runAssertions(this, () => { const msg = () => `Expected '${htmlElement.outerHTML}'${notFor(this)}to be hidden`; assert(htmlElement, 'actual does not exist'); assert( htmlElement.style.display !== 'none' && htmlElement.style.visibility !== 'hidden' && htmlElement.style.visibility !== 'collapse', msg ); return msg; }); }, async toBeCompleted(actual) { return runAssertionsAsync(this, async () => { let completed = false; let state = 'pending'; const msg = () => `expected${notFor(this)}to complete promise (final state: ${state})`; actual .then(() => { state = 'resolved'; completed = true; }) .catch(() => { state = 'rejected'; completed = true; }); await sleep(50); if (completed && this.isNot) { fail(msg()); } else if (!completed && !this.isNot) { fail(msg()); } }); }, async toBeResolved(actual, message, timeoutMs) { return runAssertionsAsync(this, async () => { let resolved = null; const timeout = timeoutMs || 50; const msg = () => `expected${notFor(this)}to resolve promise, but ${ resolved === null ? 'it never completed' : 'it rejected' }${message ? `More info: ${message}` : ''}`; actual .then(() => { resolved = true; }) .catch(() => { resolved = false; }); let slept = 0; while (resolved === null && slept < timeout) { // eslint-disable-next-line no-await-in-loop await sleep(50); slept += 50; } if (resolved && this.isNot) { fail(msg()); } else if (!resolved && !this.isNot) { fail(msg()); } }); }, async toBeRejected(actual, message, timeoutMs) { return runAssertionsAsync(this, async () => { let rejected = null; const timeout = timeoutMs || 50; const msg = () => `expected${notFor(this)}to reject promise, but ${ rejected === null ? 'it never completed' : 'it resolved' }${message ? `More info: ${message}` : ''}`; actual .then(() => { rejected = false; }) .catch(() => { rejected = true; }); let slept = 0; while (rejected === null && slept < timeout) { // eslint-disable-next-line no-await-in-loop await sleep(50); slept += 50; } if (rejected && this.isNot) { fail(msg()); } else if (!rejected && !this.isNot) { fail(msg()); } }); }, toExist(actual) { return runAssertions(this, () => { const msg = () => `Expected ${actual}${notFor(this)}to exist`; assert(actual !== null && actual !== undefined, msg); return msg; }); }, toBeDisabled(actual) { return runAssertions(this, () => { const msg = () => `Expected ${actual}${notFor(this)}to be disabled`; assert(actual.disabled, msg); return msg; }); }, toHaveReceivedNoCallsAtAll(mockedObject) { return runAssertions(this, () => { const called = Object.getOwnPropertyNames( Object.getPrototypeOf(mockedObject) ).reduce((acc, cur) => { const prop = mockedObject[cur]; if (typeof prop.mock === 'undefined') { return acc; } if (prop.mock.calls && prop.mock.calls.length) { acc.push(cur); } return acc; }, []); const msg = () => `expected${notFor( this )}to have received any calls, but got ${called}`; assert(!called.length, msg); return msg; }); }, toHaveReceivedOnly(mockedObject, ...calls) { return runAssertions(this, () => { const called = Object.getOwnPropertyNames( Object.getPrototypeOf(mockedObject) ).reduce((acc, cur) => { const prop = mockedObject[cur]; if (typeof prop.mock === 'undefined') { return acc; } if ( prop.mock.calls && prop.mock.calls.length && calls.indexOf(cur) === -1 ) { acc.push(cur); } return acc; }, []); const msg = () => `expected${notFor( this )}to have received any calls, but got ${called}`; assert(!called.length, msg); return msg; }); }, }); });