From 463d4adc6239d7ac1fab88e369317e831f5efe89 Mon Sep 17 00:00:00 2001 From: offirgolan Date: Tue, 9 Apr 2019 15:47:11 -0700 Subject: [PATCH 1/6] feat(core): Add times option for events and the intercept handler --- .../core/src/-private/event-emitter.js | 43 +++++++++++++------ packages/@pollyjs/core/src/server/handler.js | 31 +++++++++++-- packages/@pollyjs/core/src/server/route.js | 10 ++--- .../core/src/utils/cancel-fn-after-n-times.js | 21 +++++++++ .../@pollyjs/core/src/utils/validators.js | 12 ++++++ 5 files changed, 97 insertions(+), 20 deletions(-) create mode 100644 packages/@pollyjs/core/src/utils/cancel-fn-after-n-times.js diff --git a/packages/@pollyjs/core/src/-private/event-emitter.js b/packages/@pollyjs/core/src/-private/event-emitter.js index 9f4ed39d..c442a6d7 100644 --- a/packages/@pollyjs/core/src/-private/event-emitter.js +++ b/packages/@pollyjs/core/src/-private/event-emitter.js @@ -1,6 +1,9 @@ import { assert } from '@pollyjs/utils'; import isObjectLike from 'lodash-es/isObjectLike'; +import cancelFnAfterNTimes from '../utils/cancel-fn-after-n-times'; +import { validateTimesOption } from '../utils/validators'; + const EVENTS = Symbol(); const EVENT_NAMES = Symbol(); @@ -64,18 +67,35 @@ export default class EventEmitter { * * @param {String} eventName - The name of the event * @param {Function} listener - The callback function + * @param {Object} [options={}] + * @param {Number} options.times - listener will be cancelled after this many times * @returns {EventEmitter} */ - on(eventName, listener) { + on(eventName, listener, options = {}) { assertEventName(eventName, this[EVENT_NAMES]); assertListener(listener); const events = this[EVENTS]; + const { times } = options; if (!events.has(eventName)) { events.set(eventName, new Set()); } + if (times) { + validateTimesOption(times); + + const tempListener = cancelFnAfterNTimes(listener, times, () => + this.off(eventName, tempListener) + ); + + // Save the original listener on the temp one so we can easily match it + // given the original. + tempListener.listener = listener; + + listener = tempListener; + } + events.get(eventName).add(listener); return this; @@ -88,19 +108,11 @@ export default class EventEmitter { * * @param {String} eventName - The name of the event * @param {Function} listener - The callback function + * @param {Object} [options={}] * @returns {EventEmitter} */ - once(eventName, listener) { - assertEventName(eventName, this[EVENT_NAMES]); - assertListener(listener); - - const once = (...args) => { - this.off(eventName, once); - - return listener(...args); - }; - - this.on(eventName, once); + once(eventName, listener, options = {}) { + this.on(eventName, listener, { ...options, times: 1 }); return this; } @@ -122,6 +134,13 @@ export default class EventEmitter { if (this.hasListeners(eventName)) { if (typeof listener === 'function') { events.get(eventName).delete(listener); + + // Remove any wrapped listeners that use the provided listener + this.listeners(eventName).forEach(l => { + if (l.listener === listener) { + events.get(eventName).delete(l); + } + }); } else { events.get(eventName).clear(eventName); } diff --git a/packages/@pollyjs/core/src/server/handler.js b/packages/@pollyjs/core/src/server/handler.js index 8efe837c..e4b7cd89 100644 --- a/packages/@pollyjs/core/src/server/handler.js +++ b/packages/@pollyjs/core/src/server/handler.js @@ -1,7 +1,9 @@ import { assert } from '@pollyjs/utils'; import EventEmitter from '../-private/event-emitter'; +import cancelFnAfterNTimes from '../utils/cancel-fn-after-n-times'; import { + validateTimesOption, validateRecordingName, validateRequestConfig } from '../utils/validators'; @@ -11,7 +13,9 @@ export default class Handler extends Map { super(); this.set('config', {}); + this.set('defaultOptions', {}); this.set('filters', new Set()); + this._eventEmitter = new EventEmitter({ eventNames: [ 'error', @@ -24,8 +28,11 @@ export default class Handler extends Map { }); } - on(eventName, listener) { - this._eventEmitter.on(eventName, listener); + on(eventName, listener, options = {}) { + this._eventEmitter.on(eventName, listener, { + ...this.get('defaultOptions'), + ...options + }); return this; } @@ -52,12 +59,19 @@ export default class Handler extends Map { return this; } - intercept(fn) { + intercept(fn, options = {}) { assert( `Invalid intercept handler provided. Expected function, received: "${typeof fn}".`, typeof fn === 'function' ); + const { times } = { ...this.get('defaultOptions'), ...options }; + + if (times) { + validateTimesOption(times); + fn = cancelFnAfterNTimes(fn, times, () => this.delete('intercept')); + } + this.set('intercept', fn); this.passthrough(false); @@ -91,4 +105,15 @@ export default class Handler extends Map { return this; } + + times(n) { + if (!n && typeof n !== 'number') { + delete this.get('defaultOptions').times; + } else { + validateTimesOption(n); + this.get('defaultOptions').times = n; + } + + return this; + } } diff --git a/packages/@pollyjs/core/src/server/route.js b/packages/@pollyjs/core/src/server/route.js index 133b9d81..30064aae 100644 --- a/packages/@pollyjs/core/src/server/route.js +++ b/packages/@pollyjs/core/src/server/route.js @@ -97,11 +97,11 @@ export default class Route { */ async emit(eventName, req, ...args) { for (const { route, handler } of this[HANDLERS]) { - const listeners = handler._eventEmitter.listeners(eventName); - - for (const listener of listeners) { - await listener(requestWithParams(req, route), ...args); - } + await handler._eventEmitter.emit( + eventName, + requestWithParams(req, route), + ...args + ); } } diff --git a/packages/@pollyjs/core/src/utils/cancel-fn-after-n-times.js b/packages/@pollyjs/core/src/utils/cancel-fn-after-n-times.js new file mode 100644 index 00000000..af581043 --- /dev/null +++ b/packages/@pollyjs/core/src/utils/cancel-fn-after-n-times.js @@ -0,0 +1,21 @@ +/** + * Create a function that will execute the given fn and call the cancel + * callback after being called n times. + * + * @export + * @param {Function} fn + * @param {Number} nTimes + * @param {Function} cancel + * @returns + */ +export default function cancelFnAfterNTimes(fn, nTimes, cancel) { + let callCount = 0; + + return function(...args) { + if (++callCount >= nTimes) { + cancel(); + } + + return fn(...args); + }; +} diff --git a/packages/@pollyjs/core/src/utils/validators.js b/packages/@pollyjs/core/src/utils/validators.js index 4c6739a8..13b3062c 100644 --- a/packages/@pollyjs/core/src/utils/validators.js +++ b/packages/@pollyjs/core/src/utils/validators.js @@ -33,3 +33,15 @@ export function validateRequestConfig(config) { ) ); } + +export function validateTimesOption(times) { + assert( + `Invalid number provided. Expected number, received: "${typeof times}".`, + typeof times === 'number' + ); + + assert( + `Invalid number provided. The number must be greater than 0, received "${typeof times}".`, + times > 0 + ); +} From 131236ddd149a4deed12e545acfb06966143cbd2 Mon Sep 17 00:00:00 2001 From: offirgolan Date: Wed, 10 Apr 2019 10:50:00 -0700 Subject: [PATCH 2/6] feat(core): Add Event class with ability to stop propagation --- packages/@pollyjs/adapter/src/index.js | 7 +- .../core/src/-private/event-emitter.js | 64 +++++++++++-------- packages/@pollyjs/core/src/-private/event.js | 26 ++++++++ .../@pollyjs/core/src/-private/request.js | 4 ++ .../src/server}/interceptor.js | 5 +- packages/@pollyjs/core/src/server/route.js | 12 +++- .../tests/unit/server}/interceptor-test.js | 4 +- 7 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 packages/@pollyjs/core/src/-private/event.js rename packages/@pollyjs/{adapter/src/-private => core/src/server}/interceptor.js (82%) rename packages/@pollyjs/{adapter/tests/unit/-private => core/tests/unit/server}/interceptor-test.js (92%) diff --git a/packages/@pollyjs/adapter/src/index.js b/packages/@pollyjs/adapter/src/index.js index a9dc23d8..f2278a63 100644 --- a/packages/@pollyjs/adapter/src/index.js +++ b/packages/@pollyjs/adapter/src/index.js @@ -1,6 +1,5 @@ import { ACTIONS, MODES, Serializers, assert } from '@pollyjs/utils'; -import Interceptor from './-private/interceptor'; import isExpired from './utils/is-expired'; import stringifyRequest from './utils/stringify-request'; import normalizeRecordedResponse from './utils/normalize-recorded-response'; @@ -112,11 +111,9 @@ export default class Adapter { async [REQUEST_HANDLER](pollyRequest) { const { mode } = this.polly; - let interceptor; + const { _interceptor: interceptor } = pollyRequest; if (pollyRequest.shouldIntercept) { - interceptor = new Interceptor(); - await this.intercept(pollyRequest, interceptor); if (interceptor.shouldIntercept) { @@ -127,7 +124,7 @@ export default class Adapter { if ( mode === MODES.PASSTHROUGH || pollyRequest.shouldPassthrough || - (interceptor && interceptor.shouldPassthrough) + interceptor.shouldPassthrough ) { return this.passthrough(pollyRequest); } diff --git a/packages/@pollyjs/core/src/-private/event-emitter.js b/packages/@pollyjs/core/src/-private/event-emitter.js index c442a6d7..90689ad9 100644 --- a/packages/@pollyjs/core/src/-private/event-emitter.js +++ b/packages/@pollyjs/core/src/-private/event-emitter.js @@ -4,6 +4,8 @@ import isObjectLike from 'lodash-es/isObjectLike'; import cancelFnAfterNTimes from '../utils/cancel-fn-after-n-times'; import { validateTimesOption } from '../utils/validators'; +import Event from './event'; + const EVENTS = Symbol(); const EVENT_NAMES = Symbol(); @@ -135,7 +137,7 @@ export default class EventEmitter { if (typeof listener === 'function') { events.get(eventName).delete(listener); - // Remove any wrapped listeners that use the provided listener + // Remove any temp listeners that use the provided listener this.listeners(eventName).forEach(l => { if (l.listener === listener) { events.get(eventName).delete(l); @@ -181,8 +183,8 @@ export default class EventEmitter { * `eventName`, in the order they were registered, passing the supplied * arguments to each. * - * Returns a promise that will resolve to `true` if the event had listeners, - * `false` otherwise. + * Returns a promise that will resolve to `false` if a listener stopped + * propagation, `true` otherwise. * * @async * @param {String} eventName - The name of the event @@ -192,15 +194,17 @@ export default class EventEmitter { async emit(eventName, ...args) { assertEventName(eventName, this[EVENT_NAMES]); - if (this.hasListeners(eventName)) { - for (const listener of this.listeners(eventName)) { - await listener(...args); - } + const event = new Event(eventName); + + for (const listener of this.listeners(eventName)) { + await listener(...args, event); - return true; + if (event.shouldStopPropagating) { + return false; + } } - return false; + return true; } /** @@ -208,8 +212,8 @@ export default class EventEmitter { * for the event named `eventName`, in the order they were registered, * passing the supplied arguments to each. * - * Returns a promise that will resolve to `true` if the event had listeners, - * `false` otherwise. + * Returns a promise that will resolve to `false` if a listener stopped + * propagation, `true` otherwise. * * @async * @param {String} eventName - The name of the event @@ -219,15 +223,17 @@ export default class EventEmitter { async emitParallel(eventName, ...args) { assertEventName(eventName, this[EVENT_NAMES]); - if (this.hasListeners(eventName)) { - await Promise.all( - this.listeners(eventName).map(listener => listener(...args)) - ); + const event = new Event(eventName); - return true; + await Promise.all( + this.listeners(eventName).map(listener => listener(...args, event)) + ); + + if (event.shouldStopPropagating) { + return false; } - return false; + return true; } /** @@ -237,7 +243,7 @@ export default class EventEmitter { * * Throws if a listener's return value is promise-like. * - * Returns `true` if the event had listeners, `false` otherwise. + * Returns`false` if a listener stopped propagation, `true` otherwise. * * @param {String} eventName - The name of the event * @param {any} ...args - The arguments to pass to the listeners @@ -246,19 +252,21 @@ export default class EventEmitter { emitSync(eventName, ...args) { assertEventName(eventName, this[EVENT_NAMES]); - if (this.hasListeners(eventName)) { - this.listeners(eventName).forEach(listener => { - const returnValue = listener(...args); + const event = new Event(eventName); - assert( - `Attempted to emit a synchronous event "${eventName}" but an asynchronous listener was called.`, - !(isObjectLike(returnValue) && typeof returnValue.then === 'function') - ); - }); + for (const listener of this.listeners(eventName)) { + const returnValue = listener(...args, event); - return true; + assert( + `Attempted to emit a synchronous event "${eventName}" but an asynchronous listener was called.`, + !(isObjectLike(returnValue) && typeof returnValue.then === 'function') + ); + + if (event.shouldStopPropagating) { + return false; + } } - return false; + return true; } } diff --git a/packages/@pollyjs/core/src/-private/event.js b/packages/@pollyjs/core/src/-private/event.js new file mode 100644 index 00000000..e2e30529 --- /dev/null +++ b/packages/@pollyjs/core/src/-private/event.js @@ -0,0 +1,26 @@ +import { assert } from '@pollyjs/utils'; + +const STOP_PROPAGATION = Symbol(); + +export default class Event { + constructor(type, props) { + assert( + `Invalid type provided. Expected a non-empty string, received: "${typeof type}".`, + type && typeof type === 'string' + ); + + Object.defineProperty(this, 'type', { value: type }); + // eslint-disable-next-line no-restricted-properties + Object.assign(this, props || {}); + + this[STOP_PROPAGATION] = false; + } + + stopPropagation() { + this[STOP_PROPAGATION] = true; + } + + get shouldStopPropagating() { + return this[STOP_PROPAGATION]; + } +} diff --git a/packages/@pollyjs/core/src/-private/request.js b/packages/@pollyjs/core/src/-private/request.js index f6eae7af..d0e37c83 100644 --- a/packages/@pollyjs/core/src/-private/request.js +++ b/packages/@pollyjs/core/src/-private/request.js @@ -12,6 +12,7 @@ import { validateRecordingName, validateRequestConfig } from '../utils/validators'; +import Interceptor from '../server/interceptor'; import HTTPBase from './http-base'; import PollyResponse from './response'; @@ -54,6 +55,9 @@ export default class PollyRequest extends HTTPBase { */ this.action = null; + // Interceptor instance to be passed to each of the intercept handlers + this._interceptor = new Interceptor(); + // Lookup the associated route for this request this[ROUTE] = polly.server.lookup(this.method, this.url); diff --git a/packages/@pollyjs/adapter/src/-private/interceptor.js b/packages/@pollyjs/core/src/server/interceptor.js similarity index 82% rename from packages/@pollyjs/adapter/src/-private/interceptor.js rename to packages/@pollyjs/core/src/server/interceptor.js index bea098d5..6dc01049 100644 --- a/packages/@pollyjs/adapter/src/-private/interceptor.js +++ b/packages/@pollyjs/core/src/server/interceptor.js @@ -1,3 +1,5 @@ +import Event from '../-private/event'; + const ABORT = Symbol(); const PASSTHROUGH = Symbol(); @@ -6,8 +8,9 @@ function setDefaults(interceptor) { interceptor[PASSTHROUGH] = false; } -export default class Interceptor { +export default class Interceptor extends Event { constructor() { + super('intercept'); setDefaults(this); } diff --git a/packages/@pollyjs/core/src/server/route.js b/packages/@pollyjs/core/src/server/route.js index 30064aae..1cad101a 100644 --- a/packages/@pollyjs/core/src/server/route.js +++ b/packages/@pollyjs/core/src/server/route.js @@ -79,7 +79,11 @@ export default class Route { */ async intercept(req, res, interceptor) { for (const { route, handler } of this[HANDLERS]) { - if (handler.has('intercept') && interceptor.shouldIntercept) { + if (!interceptor.shouldIntercept || interceptor.shouldStopPropagating) { + return; + } + + if (handler.has('intercept')) { await handler.get('intercept')( requestWithParams(req, route), res, @@ -97,11 +101,15 @@ export default class Route { */ async emit(eventName, req, ...args) { for (const { route, handler } of this[HANDLERS]) { - await handler._eventEmitter.emit( + const shouldContinue = await handler._eventEmitter.emit( eventName, requestWithParams(req, route), ...args ); + + if (!shouldContinue) { + return; + } } } diff --git a/packages/@pollyjs/adapter/tests/unit/-private/interceptor-test.js b/packages/@pollyjs/core/tests/unit/server/interceptor-test.js similarity index 92% rename from packages/@pollyjs/adapter/tests/unit/-private/interceptor-test.js rename to packages/@pollyjs/core/tests/unit/server/interceptor-test.js index a287427b..4cb177c3 100644 --- a/packages/@pollyjs/adapter/tests/unit/-private/interceptor-test.js +++ b/packages/@pollyjs/core/tests/unit/server/interceptor-test.js @@ -1,6 +1,6 @@ -import Interceptor from '../../../src/-private/interceptor'; +import Interceptor from '../../../src/server/interceptor'; -describe('Unit | Private | Interceptor', function() { +describe('Unit | Server | Interceptor', function() { it('should exist', function() { expect(Interceptor).to.be.a('function'); }); From 283ab8396409540936acbdaf84995a0b1444270c Mon Sep 17 00:00:00 2001 From: offirgolan Date: Fri, 12 Apr 2019 15:14:03 -0700 Subject: [PATCH 3/6] test: Add tests --- .../tests/integration/server-test.js | 79 ++++ .../core/src/-private/event-emitter.js | 13 +- .../src/{server => -private}/interceptor.js | 2 +- .../@pollyjs/core/src/-private/request.js | 2 +- packages/@pollyjs/core/src/server/handler.js | 10 +- .../@pollyjs/core/src/utils/validators.js | 2 +- .../tests/unit/-private/event-emitter-test.js | 87 ++++- .../core/tests/unit/-private/event-test.js | 42 +++ .../{server => -private}/interceptor-test.js | 14 +- .../core/tests/unit/server/handler-test.js | 351 +++++++++++++++--- 10 files changed, 524 insertions(+), 78 deletions(-) rename packages/@pollyjs/core/src/{server => -private}/interceptor.js (94%) create mode 100644 packages/@pollyjs/core/tests/unit/-private/event-test.js rename packages/@pollyjs/core/tests/unit/{server => -private}/interceptor-test.js (76%) diff --git a/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js b/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js index 1dd10f4e..ce0cc4cf 100644 --- a/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js +++ b/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js @@ -201,6 +201,42 @@ describe('Integration | Server', function() { /Invalid filter callback provided/ ); }); + + it('.times()', async function() { + const { server } = this.polly; + let callCount = 0; + + server + .get('/ping') + .times(1) + .on('request', () => callCount++) + .times() + .intercept((req, res) => res.sendStatus(200)); + + expect((await fetch('/ping')).status).to.equal(200); + expect(callCount).to.equal(1); + + expect((await fetch('/ping')).status).to.equal(200); + expect(callCount).to.equal(1); + }); + + it('.intercept(_, { times }) & .on(_, { times })', async function() { + const { server } = this.polly; + let callCount = 0; + + const handler = server + .get('/ping') + .on('request', () => callCount++, { times: 1 }) + .intercept((req, res) => res.sendStatus(200), { times: 2 }); + + expect((await fetch('/ping')).status).to.equal(200); + expect(callCount).to.equal(1); + + expect((await fetch('/ping')).status).to.equal(200); + expect(callCount).to.equal(1); + + expect(handler.has('intercept')).to.be.false; + }); }); describe('Events & Middleware', function() { @@ -412,4 +448,47 @@ describe('Integration | Server', function() { expect(beforeResponseOrder).to.deep.equal([1, 2, 3, 4, 5]); }); }); + + describe('Control Flow', function() { + it('can control flow with .times() & .stopPropagation()', async function() { + const { server } = this.polly; + let calledBeforeDelete = false; + let calledAfterDelete = false; + + // First call should return the user and not enter the 2nd handler + server + .get('/user/1') + .times(1) + .on('request', (req, e) => { + e.stopPropagation(); + calledBeforeDelete = true; + }) + .intercept((req, res, interceptor) => { + interceptor.stopPropagation(); + res.sendStatus(200); + }); + + server.delete('/user/1').intercept((req, res) => res.sendStatus(201)); + + // Second call should 404 since the user no longer exists + server + .get('/user/1') + .times(1) + .on('request', () => (calledAfterDelete = true)) + .intercept((req, res) => res.sendStatus(404)); + + expect((await fetch('/user/1')).status).to.equal(200); + expect(calledBeforeDelete).to.be.true; + expect(calledAfterDelete).to.be.false; + + calledBeforeDelete = false; + expect((await fetch('/user/1', { method: 'DELETE' })).status).to.equal( + 201 + ); + + expect((await fetch('/user/1')).status).to.equal(404); + expect(calledBeforeDelete).to.be.false; + expect(calledAfterDelete).to.be.true; + }); + }); }); diff --git a/packages/@pollyjs/core/src/-private/event-emitter.js b/packages/@pollyjs/core/src/-private/event-emitter.js index 90689ad9..94d68b88 100644 --- a/packages/@pollyjs/core/src/-private/event-emitter.js +++ b/packages/@pollyjs/core/src/-private/event-emitter.js @@ -91,10 +91,21 @@ export default class EventEmitter { this.off(eventName, tempListener) ); + /* + Remove any existing listener or tempListener that match this one. + + For example, if the following would get called: + this.on('request', listener); + this.on('request', listener, { times: 1 }); + + We want to make sure that there is only one instance of the given + listener for the given event. + */ + this.off(eventName, listener); + // Save the original listener on the temp one so we can easily match it // given the original. tempListener.listener = listener; - listener = tempListener; } diff --git a/packages/@pollyjs/core/src/server/interceptor.js b/packages/@pollyjs/core/src/-private/interceptor.js similarity index 94% rename from packages/@pollyjs/core/src/server/interceptor.js rename to packages/@pollyjs/core/src/-private/interceptor.js index 6dc01049..31fc7e7a 100644 --- a/packages/@pollyjs/core/src/server/interceptor.js +++ b/packages/@pollyjs/core/src/-private/interceptor.js @@ -1,4 +1,4 @@ -import Event from '../-private/event'; +import Event from './event'; const ABORT = Symbol(); const PASSTHROUGH = Symbol(); diff --git a/packages/@pollyjs/core/src/-private/request.js b/packages/@pollyjs/core/src/-private/request.js index d0e37c83..e4d6f45d 100644 --- a/packages/@pollyjs/core/src/-private/request.js +++ b/packages/@pollyjs/core/src/-private/request.js @@ -12,11 +12,11 @@ import { validateRecordingName, validateRequestConfig } from '../utils/validators'; -import Interceptor from '../server/interceptor'; import HTTPBase from './http-base'; import PollyResponse from './response'; import EventEmitter from './event-emitter'; +import Interceptor from './interceptor'; const { keys, freeze } = Object; diff --git a/packages/@pollyjs/core/src/server/handler.js b/packages/@pollyjs/core/src/server/handler.js index e4b7cd89..04b218a8 100644 --- a/packages/@pollyjs/core/src/server/handler.js +++ b/packages/@pollyjs/core/src/server/handler.js @@ -65,11 +65,13 @@ export default class Handler extends Map { typeof fn === 'function' ); - const { times } = { ...this.get('defaultOptions'), ...options }; + options = { ...this.get('defaultOptions'), ...options }; - if (times) { - validateTimesOption(times); - fn = cancelFnAfterNTimes(fn, times, () => this.delete('intercept')); + if ('times' in options) { + validateTimesOption(options.times); + fn = cancelFnAfterNTimes(fn, options.times, () => + this.delete('intercept') + ); } this.set('intercept', fn); diff --git a/packages/@pollyjs/core/src/utils/validators.js b/packages/@pollyjs/core/src/utils/validators.js index 13b3062c..d515c1e8 100644 --- a/packages/@pollyjs/core/src/utils/validators.js +++ b/packages/@pollyjs/core/src/utils/validators.js @@ -16,7 +16,7 @@ export function validateRecordingName(name) { export function validateRequestConfig(config) { assert( `Invalid config provided. Expected object, received: "${typeof config}".`, - isObjectLike(config) + isObjectLike(config) && !Array.isArray(config) ); // The following options cannot be overridden on a per request basis diff --git a/packages/@pollyjs/core/tests/unit/-private/event-emitter-test.js b/packages/@pollyjs/core/tests/unit/-private/event-emitter-test.js index c577b244..f0812fd7 100644 --- a/packages/@pollyjs/core/tests/unit/-private/event-emitter-test.js +++ b/packages/@pollyjs/core/tests/unit/-private/event-emitter-test.js @@ -74,6 +74,35 @@ describe('Unit | EventEmitter', function() { expect(listenerCalled).to.equal(1); }); + it('.on(listener, { times })', async function() { + assertEventName('on'); + assertListener('on'); + + let listenerCalled = 0; + const listener = () => listenerCalled++; + + expect(() => emitter.on('a', listener, { times: '1' })).to.throw( + Error, + /Invalid number provided/ + ); + + expect(() => emitter.on('a', listener, { times: -1 })).to.throw( + Error, + /The number must be greater than 0/ + ); + + emitter.on('a', listener, { times: 1 }); + emitter.on('a', listener, { times: 2 }); + expect(emitter.listeners('a')).to.have.lengthOf(1); + + await emitter.emit('a'); + await emitter.emit('a'); + await emitter.emit('a'); + + expect(listenerCalled).to.equal(2); + expect(emitter.listeners('a')).to.have.lengthOf(0); + }); + it('.once()', async function() { assertEventName('once'); assertListener('once'); @@ -90,6 +119,12 @@ describe('Unit | EventEmitter', function() { await emitter.emit('a'); expect(listenerCalled).to.equal(1); + + emitter.once('a', listener); + expect(emitter.listeners('a')).to.have.lengthOf(1); + + emitter.off('a', listener); + expect(emitter.listeners('a')).to.have.lengthOf(0); }); it('.off()', async function() { @@ -107,6 +142,12 @@ describe('Unit | EventEmitter', function() { emitter.off('a'); expect(emitter.listeners('a')).to.have.lengthOf(0); + + emitter.on('a', listener, { times: 3 }); + expect(emitter.listeners('a')).to.have.lengthOf(1); + + emitter.off('a', listener); + expect(emitter.listeners('a')).to.have.lengthOf(0); }); it('.listeners()', async function() { @@ -141,8 +182,6 @@ describe('Unit | EventEmitter', function() { it('.emit()', async function() { expect(emitter.emit('a')).to.be.a('promise'); - // No listeners should resolve to `false` - expect(await emitter.emit('a')).to.be.false; const array = []; @@ -157,10 +196,21 @@ describe('Unit | EventEmitter', function() { expect(array).to.have.ordered.members([1, 2, 3]); }); + it('.emit() - stopPropagation', async function() { + const array = []; + + emitter.on('a', async e => { + e.stopPropagation(); + array.push(1); + }); + emitter.on('a', () => array.push(2)); + + expect(await emitter.emit('a')).to.be.false; + expect(array).to.have.ordered.members([1]); + }); + it('.emitParallel()', async function() { expect(emitter.emitParallel('a')).to.be.a('promise'); - // No listeners should resolve to `false` - expect(await emitter.emitParallel('a')).to.be.false; const array = []; @@ -178,10 +228,20 @@ describe('Unit | EventEmitter', function() { expect(array).to.have.ordered.members([1, 3, 2]); }); - it('.emitSync()', async function() { - // No listeners should resolve to `false` - expect(emitter.emitSync('a')).to.be.false; + it('.emitParallel() - stopPropagation', async function() { + const array = []; + emitter.on('a', async e => { + e.stopPropagation(); + array.push(1); + }); + emitter.on('a', () => array.push(2)); + + expect(await emitter.emitParallel('a')).to.be.false; + expect(array).to.have.ordered.members([1, 2]); + }); + + it('.emitSync()', async function() { emitter.once('a', () => Promise.resolve()); expect(() => emitter.emitSync('a')).to.throw( Error, @@ -197,5 +257,18 @@ describe('Unit | EventEmitter', function() { expect(emitter.emitSync('a')).to.be.true; expect(array).to.have.ordered.members([1, 2, 3]); }); + + it('.emitSync() - stopPropagation', async function() { + const array = []; + + emitter.on('a', e => { + e.stopPropagation(); + array.push(1); + }); + emitter.on('a', () => array.push(2)); + + expect(emitter.emitSync('a')).to.be.false; + expect(array).to.have.ordered.members([1]); + }); }); }); diff --git a/packages/@pollyjs/core/tests/unit/-private/event-test.js b/packages/@pollyjs/core/tests/unit/-private/event-test.js new file mode 100644 index 00000000..47f7c9ea --- /dev/null +++ b/packages/@pollyjs/core/tests/unit/-private/event-test.js @@ -0,0 +1,42 @@ +import Event from '../../../src/-private/event'; + +const EVENT_TYPE = 'foo'; + +describe('Unit | Event', function() { + it('should exist', function() { + expect(Event).to.be.a('function'); + }); + + it('should throw if no type is specified', function() { + expect(() => new Event()).to.throw(Error, /Invalid type provided/); + }); + + it('should have the correct defaults', function() { + const event = new Event(EVENT_TYPE); + + expect(event.type).to.equal(EVENT_TYPE); + expect(event.shouldStopPropagating).to.be.false; + }); + + it('should not be able to edit the type', function() { + const event = new Event(EVENT_TYPE); + + expect(event.type).to.equal(EVENT_TYPE); + expect(() => (event.type = 'bar')).to.throw(Error); + }); + + it('should be able to attach any other properties', function() { + const event = new Event(EVENT_TYPE, { foo: 1, bar: 2 }); + + expect(event.foo).to.equal(1); + expect(event.bar).to.equal(2); + }); + + it('.stopPropagation()', function() { + const event = new Event(EVENT_TYPE); + + expect(event.shouldStopPropagating).to.be.false; + event.stopPropagation(); + expect(event.shouldStopPropagating).to.be.true; + }); +}); diff --git a/packages/@pollyjs/core/tests/unit/server/interceptor-test.js b/packages/@pollyjs/core/tests/unit/-private/interceptor-test.js similarity index 76% rename from packages/@pollyjs/core/tests/unit/server/interceptor-test.js rename to packages/@pollyjs/core/tests/unit/-private/interceptor-test.js index 4cb177c3..b219d520 100644 --- a/packages/@pollyjs/core/tests/unit/server/interceptor-test.js +++ b/packages/@pollyjs/core/tests/unit/-private/interceptor-test.js @@ -1,6 +1,6 @@ -import Interceptor from '../../../src/server/interceptor'; +import Interceptor from '../../../src/-private/interceptor'; -describe('Unit | Server | Interceptor', function() { +describe('Unit | Interceptor', function() { it('should exist', function() { expect(Interceptor).to.be.a('function'); }); @@ -8,9 +8,11 @@ describe('Unit | Server | Interceptor', function() { it('should have the correct defaults', function() { const interceptor = new Interceptor(); + expect(interceptor.type).to.equal('intercept'); expect(interceptor.shouldAbort).to.be.false; expect(interceptor.shouldPassthrough).to.be.false; expect(interceptor.shouldIntercept).to.be.true; + expect(interceptor.shouldStopPropagating).to.be.false; }); it('should disable passthrough when calling abort and vise versa', function() { @@ -51,4 +53,12 @@ describe('Unit | Server | Interceptor', function() { expect(interceptor.shouldPassthrough).to.be.true; expect(interceptor.shouldIntercept).to.be.false; }); + + it('.stopPropagation()', function() { + const interceptor = new Interceptor(); + + expect(interceptor.shouldStopPropagating).to.be.false; + interceptor.stopPropagation(); + expect(interceptor.shouldStopPropagating).to.be.true; + }); }); diff --git a/packages/@pollyjs/core/tests/unit/server/handler-test.js b/packages/@pollyjs/core/tests/unit/server/handler-test.js index 6c53e2d5..06e5c5a3 100644 --- a/packages/@pollyjs/core/tests/unit/server/handler-test.js +++ b/packages/@pollyjs/core/tests/unit/server/handler-test.js @@ -5,89 +5,318 @@ describe('Unit | Server | Handler', function() { expect(Handler).to.be.a('function'); }); - it('throws on registering an unknown event name', function() { - expect(() => new Handler().on('unknownEventName')).to.throw( - /Invalid event name provided/ - ); - }); + describe('Events', function() { + it('throws on registering an unknown event name', function() { + expect(() => new Handler().on('unknownEventName')).to.throw( + /Invalid event name provided/ + ); + }); + + it('throws on un-registering an unknown event name', function() { + expect(() => new Handler().off('unknownEventName')).to.throw( + /Invalid event name provided/ + ); + }); + + it('registers a known event via .on()', function() { + const handler = new Handler(); + const { _eventEmitter: eventEmitter } = handler; + + expect(eventEmitter.hasListeners('request')).to.be.false; + + handler.on('request', () => {}); + expect(eventEmitter.hasListeners('request')).to.be.true; + + handler.on('request', () => {}); + expect(eventEmitter.listeners('request')).to.have.lengthOf(2); + }); + + it('registers a known event via .on() with { times }', function() { + const handler = new Handler(); + const { _eventEmitter: eventEmitter } = handler; + + handler.on('request', () => {}, { times: 2 }); + expect(eventEmitter.hasListeners('request')).to.be.true; + + eventEmitter.emitSync('request'); + expect(eventEmitter.hasListeners('request')).to.be.true; + + eventEmitter.emitSync('request'); + expect(eventEmitter.hasListeners('request')).to.be.false; + }); + + it('registers a known event via .on() with .times()', function() { + const handler = new Handler(); + const { _eventEmitter: eventEmitter } = handler; + + handler.times(2).on('request', () => {}); + expect(eventEmitter.hasListeners('request')).to.be.true; + + eventEmitter.emitSync('request'); + expect(eventEmitter.hasListeners('request')).to.be.true; + + eventEmitter.emitSync('request'); + expect(eventEmitter.hasListeners('request')).to.be.false; + }); + + it('registers a known event via .once()', function() { + const handler = new Handler(); + const { _eventEmitter: eventEmitter } = handler; - it('throws on un-registering an unknown event name', function() { - expect(() => new Handler().off('unknownEventName')).to.throw( - /Invalid event name provided/ - ); + expect(eventEmitter.hasListeners('request')).to.be.false; + + handler.once('request', () => {}); + expect(eventEmitter.hasListeners('request')).to.be.true; + + handler.once('request', () => {}); + expect(eventEmitter.listeners('request')).to.have.lengthOf(2); + + eventEmitter.emitSync('request'); + expect(eventEmitter.hasListeners('request')).to.be.false; + }); + + it('un-registers a known event via .off()', function() { + const handler = new Handler(); + const { _eventEmitter: eventEmitter } = handler; + const fn = () => {}; + + handler.on('request', fn); + handler.on('request', () => {}); + handler.on('request', () => {}); + expect(eventEmitter.hasListeners('request')).to.be.true; + expect(eventEmitter.listeners('request')).to.have.lengthOf(3); + + handler.off('request', fn); + expect(eventEmitter.hasListeners('request')).to.be.true; + expect(eventEmitter.listeners('request')).to.have.lengthOf(2); + expect(eventEmitter.listeners('request').includes(fn)).to.be.false; + + handler.off('request'); + expect(eventEmitter.hasListeners('request')).to.be.false; + expect(eventEmitter.listeners('request')).to.have.lengthOf(0); + }); }); - it('registers a known event via .on()', function() { - const handler = new Handler(); - const { _eventEmitter: eventEmitter } = handler; + describe('.intercept()', function() { + it('registers an intercept handler', function() { + const handler = new Handler(); + + handler.intercept(() => {}); + expect(handler.has('intercept')).to.be.true; + }); + + it('throws when passing a non-function to intercept', function() { + const handler = new Handler(); + + [null, undefined, {}, [], ''].forEach(value => { + expect(() => handler.intercept(value)).to.throw( + /Invalid intercept handler provided/ + ); + }); + }); + + it('throws when passing an invalid times option', function() { + const handler = new Handler(); + + ['1', -1, 0].forEach(times => { + expect(() => handler.intercept(() => {}, { times })).to.throw( + /Invalid number provided/ + ); + }); + }); + + it('registers an intercept handler with { times }', function() { + const handler = new Handler(); + + handler.intercept(() => {}, { times: 2 }); + expect(handler.has('intercept')).to.be.true; + + handler.get('intercept')(); + expect(handler.has('intercept')).to.be.true; + + handler.get('intercept')(); + expect(handler.has('intercept')).to.be.false; + }); + + it('registers an intercept handler with .times()', function() { + const handler = new Handler(); - expect(eventEmitter.hasListeners('request')).to.be.false; + handler.times(2).intercept(() => {}); + expect(handler.has('intercept')).to.be.true; - handler.on('request', () => {}); - expect(eventEmitter.hasListeners('request')).to.be.true; + handler.get('intercept')(); + expect(handler.has('intercept')).to.be.true; - handler.on('request', () => {}); - expect(eventEmitter.listeners('request')).to.have.lengthOf(2); + handler.get('intercept')(); + expect(handler.has('intercept')).to.be.false; + }); }); - it('un-registers a known event via .off()', function() { - const handler = new Handler(); - const { _eventEmitter: eventEmitter } = handler; - const fn = () => {}; - - handler.on('request', fn); - handler.on('request', () => {}); - handler.on('request', () => {}); - expect(eventEmitter.hasListeners('request')).to.be.true; - expect(eventEmitter.listeners('request')).to.have.lengthOf(3); - - handler.off('request', fn); - expect(eventEmitter.hasListeners('request')).to.be.true; - expect(eventEmitter.listeners('request')).to.have.lengthOf(2); - expect(eventEmitter.listeners('request').includes(fn)).to.be.false; - - handler.off('request'); - expect(eventEmitter.hasListeners('request')).to.be.false; - expect(eventEmitter.listeners('request')).to.have.lengthOf(0); + describe('.passthrough()', function() { + it('should work', function() { + const handler = new Handler(); + + expect(handler.has('passthrough')).to.be.false; + + handler.passthrough(); + expect(handler.get('passthrough')).to.be.true; + + handler.passthrough(false); + expect(handler.get('passthrough')).to.be.false; + }); + + it('removes the intercept handler on passthrough', function() { + const handler = new Handler(); + + handler.intercept(() => {}); + expect(handler.has('intercept')).to.be.true; + + handler.passthrough(); + expect(handler.get('passthrough')).to.be.true; + expect(handler.has('intercept')).to.be.false; + }); + + it('disables passthrough on intercept', function() { + const handler = new Handler(); + + handler.passthrough(); + expect(handler.get('passthrough')).to.be.true; + expect(handler.has('intercept')).to.be.false; + + handler.intercept(() => {}); + expect(handler.has('intercept')).to.be.true; + expect(handler.get('passthrough')).to.be.false; + }); }); - it('registers an intercept handler', function() { - const handler = new Handler(); + describe('.recordingName()', function() { + it('should work', function() { + const handler = new Handler(); + + expect(handler.has('recordingName')).to.be.false; + + handler.recordingName('Test'); + expect(handler.get('recordingName')).to.equal('Test'); + + handler.recordingName(); + expect(handler.has('recordingName')).to.be.true; + expect(handler.get('recordingName')).to.be.undefined; + }); + + it('should allow setting a falsy recordingName', function() { + const handler = new Handler(); + + expect(handler.has('recordingName')).to.be.false; + + [false, undefined, null].forEach(value => { + handler.recordingName(value); + expect(handler.has('recordingName')).to.be.true; + expect(handler.get('recordingName')).to.equal(value); + }); + }); + + it('throws when passing an invalid truthy recording name', function() { + const handler = new Handler(); - handler.intercept(() => {}); - expect(handler.has('intercept')).to.be.true; + [1, {}, [], true].forEach(value => { + expect(() => handler.recordingName(value)).to.throw( + /Invalid recording name provided/ + ); + }); + }); }); - it('throws when passing a non-function to intercept', function() { - const handler = new Handler(); + describe('.configure()', function() { + it('should work', function() { + const handler = new Handler(); - [null, undefined, {}, [], ''].forEach(value => { - expect(() => handler.intercept(value)).to.throw( - /Invalid intercept handler provided/ - ); + expect(handler.get('config')).to.deep.equal({}); + + handler.configure({ logging: true }); + expect(handler.get('config')).to.deep.equal({ logging: true }); + + handler.configure({ recordIfMissing: false }); + expect(handler.get('config')).to.deep.equal({ recordIfMissing: false }); + + handler.configure({}); + expect(handler.get('config')).to.deep.equal({}); + }); + + it('throws when passing an invalid config', function() { + const handler = new Handler(); + + [false, true, null, undefined, 1, []].forEach(config => { + expect(() => handler.configure(config)).to.throw( + /Invalid config provided/ + ); + }); + + [ + 'mode', + 'adapters', + 'adapterOptions', + 'persister', + 'persisterOptions' + ].forEach(key => { + expect(() => handler.configure({ [key]: key })).to.throw( + /Invalid configuration option provided/ + ); + }); }); }); - it('removes the intercept handler on passthrough', function() { - const handler = new Handler(); + describe('.filter()', function() { + it('should work', function() { + const handler = new Handler(); + const filters = handler.get('filters'); + const fn = () => {}; + + expect(filters.size).to.equal(0); - handler.intercept(() => {}); - expect(handler.has('intercept')).to.be.true; + handler.filter(fn); + expect(filters.size).to.equal(1); - handler.passthrough(); - expect(handler.get('passthrough')).to.be.true; - expect(handler.has('intercept')).to.be.false; + handler.filter(fn); + expect(filters.size).to.equal(1); + + handler.filter(() => {}); + expect(filters.size).to.equal(2); + }); + + it('throws when passing an invalid fn', function() { + const handler = new Handler(); + + [false, true, null, undefined, 1, [], {}, ''].forEach(fn => { + expect(() => handler.filter(fn)).to.throw( + /Invalid filter callback provided/ + ); + }); + }); }); - it('disables passthrough on intercept', function() { - const handler = new Handler(); + describe('.times()', function() { + it('should work', function() { + const handler = new Handler(); + const defaultOptions = handler.get('defaultOptions'); + + expect(defaultOptions).to.deep.equal({}); - handler.passthrough(); - expect(handler.get('passthrough')).to.be.true; - expect(handler.has('intercept')).to.be.false; + handler.times(1); + expect(defaultOptions).to.deep.equal({ times: 1 }); - handler.intercept(() => {}); - expect(handler.has('intercept')).to.be.true; - expect(handler.get('passthrough')).to.be.false; + handler.times(2); + expect(defaultOptions).to.deep.equal({ times: 2 }); + + handler.times(); + expect(defaultOptions).to.deep.equal({}); + }); + + it('throws when passing an invalid times option', function() { + const handler = new Handler(); + + ['1', -1, 0].forEach(times => { + expect(() => handler.times(times)).to.throw(/Invalid number provided/); + }); + }); }); }); From f4339a6b96a2955a7471b562e9f409d832a7832a Mon Sep 17 00:00:00 2001 From: offirgolan Date: Thu, 25 Apr 2019 15:06:59 -0700 Subject: [PATCH 4/6] test: Add override { times } tests --- .../tests/integration/server-test.js | 14 ++++++------- .../core/tests/unit/server/handler-test.js | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js b/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js index ce0cc4cf..be4a7fbd 100644 --- a/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js +++ b/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js @@ -457,7 +457,7 @@ describe('Integration | Server', function() { // First call should return the user and not enter the 2nd handler server - .get('/user/1') + .get('/users/1') .times(1) .on('request', (req, e) => { e.stopPropagation(); @@ -468,25 +468,25 @@ describe('Integration | Server', function() { res.sendStatus(200); }); - server.delete('/user/1').intercept((req, res) => res.sendStatus(201)); + server.delete('/users/1').intercept((req, res) => res.sendStatus(204)); // Second call should 404 since the user no longer exists server - .get('/user/1') + .get('/users/1') .times(1) .on('request', () => (calledAfterDelete = true)) .intercept((req, res) => res.sendStatus(404)); - expect((await fetch('/user/1')).status).to.equal(200); + expect((await fetch('/users/1')).status).to.equal(200); expect(calledBeforeDelete).to.be.true; expect(calledAfterDelete).to.be.false; calledBeforeDelete = false; - expect((await fetch('/user/1', { method: 'DELETE' })).status).to.equal( - 201 + expect((await fetch('/users/1', { method: 'DELETE' })).status).to.equal( + 204 ); - expect((await fetch('/user/1')).status).to.equal(404); + expect((await fetch('/users/1')).status).to.equal(404); expect(calledBeforeDelete).to.be.false; expect(calledAfterDelete).to.be.true; }); diff --git a/packages/@pollyjs/core/tests/unit/server/handler-test.js b/packages/@pollyjs/core/tests/unit/server/handler-test.js index 06e5c5a3..18c91214 100644 --- a/packages/@pollyjs/core/tests/unit/server/handler-test.js +++ b/packages/@pollyjs/core/tests/unit/server/handler-test.js @@ -59,6 +59,17 @@ describe('Unit | Server | Handler', function() { expect(eventEmitter.hasListeners('request')).to.be.false; }); + it('registers a known event via .on() with .times() and override with { times }', function() { + const handler = new Handler(); + const { _eventEmitter: eventEmitter } = handler; + + handler.times(2).on('request', () => {}, { times: 1 }); + expect(eventEmitter.hasListeners('request')).to.be.true; + + eventEmitter.emitSync('request'); + expect(eventEmitter.hasListeners('request')).to.be.false; + }); + it('registers a known event via .once()', function() { const handler = new Handler(); const { _eventEmitter: eventEmitter } = handler; @@ -150,6 +161,16 @@ describe('Unit | Server | Handler', function() { handler.get('intercept')(); expect(handler.has('intercept')).to.be.false; }); + + it('registers an intercept handler with .times() and override with { times }', function() { + const handler = new Handler(); + + handler.times(2).intercept(() => {}, { times: 1 }); + expect(handler.has('intercept')).to.be.true; + + handler.get('intercept')(); + expect(handler.has('intercept')).to.be.false; + }); }); describe('.passthrough()', function() { From 1eb444627d68a46e3b779acbea61391fb7a04e03 Mon Sep 17 00:00:00 2001 From: offirgolan Date: Thu, 25 Apr 2019 15:45:01 -0700 Subject: [PATCH 5/6] docs: Update docs --- docs/_sidebar.md | 1 + docs/assets/styles.css | 5 + docs/server/event.md | 29 +++++ docs/server/events-and-middleware.md | 6 + docs/server/request.md | 170 +++++++++++++-------------- docs/server/route-handler.md | 86 ++++++++++++-- 6 files changed, 204 insertions(+), 93 deletions(-) create mode 100644 docs/server/event.md diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 5decf713..468133c8 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -37,6 +37,7 @@ - [Route Handler](server/route-handler.md) - [Request](server/request.md) - [Response](server/response.md) + - [Event](server/event.md) - Node Server diff --git a/docs/assets/styles.css b/docs/assets/styles.css index 676b3d24..fa0af15e 100644 --- a/docs/assets/styles.css +++ b/docs/assets/styles.css @@ -354,3 +354,8 @@ body .sidebar-toggle span:nth-child(3) { .lang-json .token.property { color: #e96900; } + +/****** COPY TO CLIPBOARD ******/ +.docsify-copy-code-button { + font-size: 0.7em !important; +} diff --git a/docs/server/event.md b/docs/server/event.md new file mode 100644 index 00000000..fbad3b7d --- /dev/null +++ b/docs/server/event.md @@ -0,0 +1,29 @@ +# Event + +## Properties + +### type + +_Type_: `String` + +The event type. (e.g. `request`, `response`, `beforePersist`) + +## Methods + +### stopPropagation + +If several event listeners are attached to the same event type, they are called in the order in which they were added. If `stopPropagation` is invoked during one such call, no remaining listeners will be called. + +**Example** + +```js +server.get('/session/:id').on('beforeResponse', (req, res, event) => { + event.stopPropagation(); + res.setHeader('X-SESSION-ID', 'ABC123'); +}); + +server.get('/session/:id').on('beforeResponse', (req, res, event) => { + // This will never be reached + res.setHeader('X-SESSION-ID', 'XYZ456'); +}); +``` diff --git a/docs/server/events-and-middleware.md b/docs/server/events-and-middleware.md index 78e3bed4..4b1723e1 100644 --- a/docs/server/events-and-middleware.md +++ b/docs/server/events-and-middleware.md @@ -42,6 +42,7 @@ Fires right before the request goes out. | Param | Type | Description | | ----- | ------------------------- | -------------------- | | req | [Request](server/request) | The request instance | +| event | [Event](server/event) | The event instance | **Example** @@ -60,6 +61,7 @@ Fires right before the response materializes and the promise resolves. | ----- | --------------------------- | --------------------- | | req | [Request](server/request) | The request instance | | res | [Response](server/response) | The response instance | +| event | [Event](server/event) | The event instance | **Example** @@ -78,6 +80,7 @@ the response materializes and the promise resolves. | ----- | --------------------------- | --------------------- | | req | [Request](server/request) | The request instance | | res | [Response](server/response) | The response instance | +| event | [Event](server/event) | The event instance | **Example** @@ -97,6 +100,7 @@ Fires before the request/response gets persisted. | --------- | ------------------------- | ------------------------------------ | | req | [Request](server/request) | The request instance | | recording | `Object` | The recording that will be persisted | +| event | [Event](server/event) | The event instance | **Example** @@ -116,6 +120,7 @@ and before the recording materializes into a response. | --------- | ------------------------- | ----------------------- | | req | [Request](server/request) | The request instance | | recording | `Object` | The retrieved recording | +| event | [Event](server/event) | The event instance | **Example** @@ -134,6 +139,7 @@ Fires when any error gets emitted during the request life-cycle. | ----- | ------------------------- | -------------------- | | req | [Request](server/request) | The request instance | | error | Error | The error | +| event | [Event](server/event) | The event instance | **Example** diff --git a/docs/server/request.md b/docs/server/request.md index 4258aa4c..379a11c3 100644 --- a/docs/server/request.md +++ b/docs/server/request.md @@ -1,5 +1,90 @@ # Request +## Properties + +### method + +_Type_: `String` + +The request method. (e.g. `GET`, `POST`, `DELETE`) + +### url + +_Type_: `String` + +The request URL. + +### protocol + +_Type_: `String` + +The request url protocol. (e.g. `http://`, `https:`) + +### hostname + +_Type_: `String` + +The request url host name. (e.g. `localhost`, `netflix.com`) + +### port + +_Type_: `String` + +The request url port. (e.g. `3000`) + +### pathname + +_Type_: `String` + +The request url path name. (e.g. `/session`, `/movies/1`) + +### hash + +_Type_: `String` + +The request url hash. + +### headers + +_Type_: `Object` +_Default_: `{}` + +The request headers. + +### body + +_Type_: `any` + +The request body. + +### query + +_Type_: `Object` +_Default_: `{}` + +The request url query parameters. + +### params + +_Type_: `Object` +_Default_: `{}` + +The matching route's path params. + +**Example** + +```js +server.get('/movies/:id').intercept((req, res) => { + console.log(req.params.id); +}); +``` + +### recordingName + +_Type_: `String` + +The recording the request should be recorded under. + ## Methods ### getHeader @@ -161,88 +246,3 @@ A shortcut method that calls JSON.parse on the request's body. ```js req.jsonBody(); ``` - -## Properties - -### method - -_Type_: `String` - -The request method. (e.g. `GET`, `POST`, `DELETE`) - -### url - -_Type_: `String` - -The request URL. - -### protocol - -_Type_: `String` - -The request url protocol. (e.g. `http://`, `https:`) - -### hostname - -_Type_: `String` - -The request url host name. (e.g. `localhost`, `netflix.com`) - -### port - -_Type_: `String` - -The request url port. (e.g. `3000`) - -### pathname - -_Type_: `String` - -The request url path name. (e.g. `/session`, `/movies/1`) - -### hash - -_Type_: `String` - -The request url hash. - -### headers - -_Type_: `Object` -_Default_: `{}` - -The request headers. - -### body - -_Type_: `any` - -The request body. - -### query - -_Type_: `Object` -_Default_: `{}` - -The request url query parameters. - -### params - -_Type_: `Object` -_Default_: `{}` - -The matching route's path params. - -**Example** - -```js -server.get('/movies/:id').intercept((req, res) => { - console.log(req.params.id); -}); -``` - -### recordingName - -_Type_: `String` - -The recording the request should be recorded under. diff --git a/docs/server/route-handler.md b/docs/server/route-handler.md index 01fd47be..e077786d 100644 --- a/docs/server/route-handler.md +++ b/docs/server/route-handler.md @@ -15,10 +15,12 @@ Register an [event](server/events-and-middleware) handler. ?> **Tip:** You can attach multiple handlers to a single event. Handlers will be called in the order they were declared. -| Param | Type | Description | -| --------- | ---------- | ----------------- | -| eventName | `String` | The event name | -| handler | `Function` | The event handler | +| Param | Type | Description | +| ------------- | ---------- | ---------------------------------------------------------------- | +| eventName | `String` | The event name | +| handler | `Function` | The event handler | +| options | `Object` | The event handler options | +| options.times | `number` | Remove listener after being called the specified amount of times | **Example** @@ -31,7 +33,14 @@ server }) .on('request', () => { /* Do something else */ - }); + }) + .on( + 'request', + () => { + /* Do something else twice */ + }, + { times: 2 } + ); ``` ### once @@ -90,15 +99,19 @@ never go to server but instead defer to the provided handler to handle the [response](server/response). If multiple intercept handlers have been registered, each handler will be called in the order in which it was registered. -| Param | Type | Description | -| ------- | ---------- | --------------------- | -| handler | `Function` | The intercept handler | +| Param | Type | Description | +| ------------- | ---------- | --------------------------------------------------------------- | +| handler | `Function` | The intercept handler | +| options | `Object` | The event handler options | +| options.times | `number` | Remove handler after being called the specified amount of times | **Example** ```js server.any('/session').intercept((req, res) => res.sendStatus(200)); +server.any('/twice').intercept((req, res) => res.sendStatus(200), { times: 2 }); + server.get('/session/:id').intercept((req, res, interceptor) => { if (req.params.id === '1') { res.status(200).json({ token: 'ABC123XYZ' }); @@ -112,6 +125,8 @@ server.get('/session/:id').intercept((req, res, interceptor) => { #### Interceptor +_Extends [Event](server/event)_ + The `intercept` handler receives a third `interceptor` argument that provides some utilities. @@ -152,6 +167,33 @@ server.get('/session/:id').intercept((req, res, interceptor) => { }); ``` +##### stopPropagation + +If several intercept handlers are attached to the same route, they are called in the order in which they were added. If `stopPropagation` is invoked during one such call, no remaining handlers will be called. + +**Example** + +```js +// First call should return the user and not enter the 2nd handler +server + .get('/session/:id') + .times(1) // Remove this interceptor after it gets called once + .intercept((req, res, interceptor) => { + // Do not continue to the next intercept handler which handles the 404 case + interceptor.stopPropagation(); + res.sendStatus(200); + }); + +server.delete('/session/:id').intercept((req, res) => res.sendStatus(204)); + +// Second call should 404 since the user no longer exists +server.get('/session/:id').intercept((req, res) => res.sendStatus(404)); + +await fetch('/users/1'); // --> 200 +await fetch('/users/1', { method: 'DELETE' }); // --> 204 +await fetch('/users/1'); // --> 404 +``` + ### passthrough Declare a route as a passthrough meaning any request that matches that route @@ -203,6 +245,34 @@ server }); ``` +### times + +Proceeding intercept and event handlers defined will be removed after being called the specified amount of times. The number specified is used as a default value and can be overridden by passing a custom `times` option to the handler. + +| Param | Type | Description | +| ----- | -------- | -------------------------------------------------------------------------------------------------- | +| times | `number` | Default times value for proceeding handlers. If no value is provided, the default value is removed | + +**Example** + +```js +server + .any() + .times(2); + .on('request', req => {}); + .intercept((req, res) => {}); + .times() + .on('response', (req, res) => {}); + +// Is the same as: + +server + .any() + .on('request', req => {}, { times: 2 }); + .intercept((req, res) => {}, { times: 2 }); + .on('response', (req, res) => {}); +``` + ### configure Override configuration options for the given route. All matching middleware and route level configs are merged together and the overrides are applied to the current From 343f4f944f4d6708c0d8168e05ae34a773c667c7 Mon Sep 17 00:00:00 2001 From: offirgolan Date: Thu, 25 Apr 2019 15:57:46 -0700 Subject: [PATCH 6/6] test: Fix failing test case --- .../@pollyjs/adapter-fetch/tests/integration/server-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js b/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js index be4a7fbd..9464fcf0 100644 --- a/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js +++ b/packages/@pollyjs/adapter-fetch/tests/integration/server-test.js @@ -468,7 +468,7 @@ describe('Integration | Server', function() { res.sendStatus(200); }); - server.delete('/users/1').intercept((req, res) => res.sendStatus(204)); + server.delete('/users/1').intercept((req, res) => res.status(204)); // Second call should 404 since the user no longer exists server