UNPKG

fake-promise

Version:

Gives full control over when ES6 promises are resolved by providing an implementation of Promise with behavior controlled by .resolve(result) and .reject(error) methods. Intended for use in unit tests.

244 lines 9.05 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.FakePromise = void 0; let nextId = 0; const SPACES = ' '; class FakePromise { constructor() { this.id = nextId++; this.isChained = false; this.resultPromised = false; this.resolveChain = false; this.resultSet = false; this.errorSet = false; this.specified = false; this.resolved = false; this.rejected = false; } static resolve(result) { const promise = new FakePromise(); promise.resolve(result); return promise; } static reject(error) { const promise = new FakePromise(); promise.reject(error); return promise; } get promiseTrace() { return this._promiseTrace; } get resultTrace() { return this._resultTrace; } get errorTrace() { return this._errorTrace; } get specifyTrace() { return this._specifyTrace; } get resolveTrace() { return this._resolveTrace; } get rejectTrace() { return this._rejectTrace; } then(onfulfilled, onrejected) { this.check(!this.specified, 'promise already specified', this._specifyTrace); this.onfulfilled = onfulfilled; this.onrejected = onrejected; this.specified = true; this._specifyTrace = this.trace('specification'); return this.maybeFinishResolving(); } catch(onrejected) { return this.then(undefined, onrejected); } finally(onfinally) { const callback = onfinally || noop; return this.then(callback, callback); } resolve(result) { this.markResolveChain(); this.resolveOne(result); } reject(error) { this.markResolveChain(); this.rejectOne(error); } setResult(result) { this.check(!this.isChained, "result musn't be programmatically set in a chained promise"); this._setResult(result); } setError(error) { this.check(!this.isChained, "error musn't be programmatically set in a chained promise"); this._setError(error); } resolveOne(result) { this.check(!this.errorSet, 'trying to resolve a promise containing error', this._errorTrace); if (this.isChained) { this.check(result === undefined, "result musn't be programmatically set in a chained promise"); } else if (result !== undefined || !this.resultSet) { this._setResult(result); } this.markResolved(); return this.maybeFinishResolving(); } rejectOne(error) { this.check(!this.resultSet, 'trying to reject a promise containing result', this._resultTrace); if (this.isChained) { this.check(error === undefined, "error musn't be programmatically set in a chained promise"); } else if (error !== undefined || !this.errorSet) { this._setError(error); } this.markRejected(); return this.maybeFinishResolving(); } toJSON() { const { id, resultPromised, resolveChain, resultSet, errorSet, specified, resolved, rejected } = this; return { id, resultPromised, resolveChain, resultSet, errorSet, specified, resolved, rejected }; } toString(indent) { const flags = this.toJSON(); const keys = Object.keys(flags) .filter(key => key !== 'id') .filter(key => flags[key]); if (!indent) { const stringifiedFlags = keys.map(key => `${key}: ${flags[key]}`); return `FakePromise#${this.id}{${stringifiedFlags.join(',')}}`; } const prefix = SPACES.substring(0, indent); const stringifiedFlags = keys.map(key => `${prefix} ${key}: ${flags[key]},\n`); return `${prefix}FakePromise#${this.id} {\n${stringifiedFlags.join('')}${prefix}}`; } _setResult(result) { this.check(!this.errorSet, 'trying to set result on a promise with error already set', this._errorTrace); this.check(!this.resultSet, 'result already set', this._resultTrace); this.check(!this.resultPromised, 'result already set (waiting for promise)', this._promiseTrace); if (isPromise(result)) { this.resultPromised = true; this._promiseTrace = this.trace('setting promise as a result'); result.then(result => { this.resultPromised = false; this._setResult(result); }, error => { this.resultPromised = false; this._setError(error); }); return; } this.resultSet = true; this.result = result; this._resultTrace = this.trace('setting result'); this.maybeFinishResolving(); } _setError(error) { this.check(!this.resultSet, 'trying to set error on a promise with result already set', this._resultTrace); this.check(!this.errorSet, 'error already set', this._errorTrace); this.check(!this.resultPromised, 'result already set (waiting for promise)', this._promiseTrace); this.check(hasValue(error), 'error must not be undefined nor null'); this.errorSet = true; this.error = error; this._errorTrace = this.trace('setting error'); this.maybeFinishResolving(); } markResolveChain() { this.resolveChain = true; } markResolved() { this.check(!this.resolved, 'promise already resolved', this._resolveTrace); this.check(!this.rejected, 'promise already rejected', this._rejectTrace); this.resolved = true; this._resolveTrace = this.trace('resolve'); } markRejected() { this.check(!this.resolved, 'promise already resolved', this._resolveTrace); this.check(!this.rejected, 'promise already rejected', this._rejectTrace); this.rejected = true; this._rejectTrace = this.trace('reject'); } maybeFinishResolving() { if (!this.specified || !(this.resolved || this.rejected) || this.resultPromised) { return this.getNextPromise(); } if (this.errorSet) { return this.doReject(); } if (this.resultSet) { return this.doResolve(); } return this.getNextPromise(); } doResolve() { if (!hasValue(this.onfulfilled)) { return this.setNextResult(this.result); } const callback = this.onfulfilled; return this.executeAndSetNextResult(callback, this.result); } doReject() { if (!hasValue(this.onrejected)) { return this.setNextError(this.error); } const callback = this.onrejected; return this.executeAndSetNextResult(callback, this.error); } executeAndSetNextResult(callback, arg) { try { return this.setNextResult(callback(arg)); } catch (e) { if (e.name === 'FakePromiseError') { throw e; } return this.setNextError(e); } } setNextResult(result) { const next = this.getNextPromise(); next._setResult(result); if (this.resolveChain) { next.resolve(); } return next; } setNextError(error) { const next = this.getNextPromise(); next._setError(error); if (this.resolveChain) { next.reject(); } return next; } getNextPromise() { if (!this.nextPromise) { this.nextPromise = new FakePromise(); this.nextPromise.isChained = true; } return this.nextPromise; } check(condition, message, stacktrace) { if (!condition) { const formattedState = `\n CURRENT STATE:\n${this.toString(6)}`; const error = new Error(`${message}${formattedState}${stacktrace ? stacktrace : ''}`); error.name = 'FakePromiseError'; throw error; } } trace(name) { const stateTitle = `state after ${name}`.toUpperCase(); const formattedState = ` ${stateTitle}:\n${this.toString(6)}`; const error = new Error('error'); const stack = error.stack; const traceTitle = `stacktrace of ${name}`.toUpperCase(); const firstNewline = stack.indexOf('\n'); const secondNewline = stack.indexOf('\n', firstNewline + 1); const formattedStack = ` ${traceTitle}:${stack.substring(secondNewline)}\n EOS` .split('\n') .map(line => ` ${line}`) .join('\n'); return `\n${formattedState}\n${formattedStack}`; } } exports.FakePromise = FakePromise; exports.default = FakePromise; function hasValue(arg) { return (arg !== null && arg !== undefined); } function isPromise(arg) { return hasValue(arg) && typeof arg.then === 'function'; } function noop() { } //# sourceMappingURL=FakePromise.js.map