Skip to content

Commit a3113b7

Browse files
authored
feat: Wait for all handled requests to resolve via .flush() (#75)
1 parent 2c0083e commit a3113b7

File tree

12 files changed

+256
-22
lines changed

12 files changed

+256
-22
lines changed

docs/api.md

+14
Original file line numberDiff line numberDiff line change
@@ -251,3 +251,17 @@ __Example__
251251
```js
252252
polly.disconnect();
253253
```
254+
255+
### flush
256+
257+
Returns a Promise that resolves once all requests handled by Polly have resolved.
258+
259+
| Param | Type | Description |
260+
| --- | --- | --- |
261+
| Returns | `Promise` |   |
262+
263+
__Example__
264+
265+
```js
266+
await polly.flush();
267+
```

package.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@
2020
"clean": "lerna run clean --parallel",
2121
"bootstrap": "lerna bootstrap",
2222
"format": "lerna run format",
23-
"lint": "lerna exec --bail=false -- yarn run lint",
23+
"lint": "lerna exec --bail=false --stream --no-prefix -- yarn run lint",
2424
"pretest": "yarn server:build",
2525
"pretest:ci": "yarn pretest",
2626
"test": "testem",
2727
"test:ci": "testem ci",
2828
"test:build": "lerna run test:build --parallel",
2929
"test:clean": "rimraf packages/@pollyjs/*/build",
30-
"test:ember": "lerna run test --scope=@pollyjs/ember",
30+
"test:node": "mocha --opts tests/mocha.opts",
31+
"test:jest": "jest",
32+
"test:ember": "lerna run test --stream --no-prefix --scope=@pollyjs/ember",
3133
"server:build": "yarn build --scope=@pollyjs/node-server --scope=@pollyjs/utils",
3234
"docs:serve": "docsify serve ./docs",
3335
"docs:publish": "gh-pages --dist docs --dotfiles --message 'chore: Publish docs'",

packages/@pollyjs/adapter-puppeteer/src/index.js

+50-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Adapter from '@pollyjs/adapter';
22

33
const LISTENERS = Symbol();
4+
const POLLY_REQUEST = Symbol();
45
const PASSTHROUGH_PROMISE = Symbol();
56
const PASSTHROUGH_PROMISES = Symbol();
67
const PASSTHROUGH_REQ_ID_HEADER = 'x-pollyjs-passthrough-request-id';
@@ -73,24 +74,68 @@ export default class PuppeteerAdapter extends Adapter {
7374
const request = response.request();
7475

7576
// Resolve the passthrough promise with the response if it exists
76-
request[PASSTHROUGH_PROMISE] &&
77+
if (request[PASSTHROUGH_PROMISE]) {
7778
request[PASSTHROUGH_PROMISE].resolve(response);
78-
79-
delete request[PASSTHROUGH_PROMISE];
79+
delete request[PASSTHROUGH_PROMISE];
80+
}
81+
},
82+
requestfinished: request => {
83+
// Resolve the deferred pollyRequest promise if it exists
84+
if (request[POLLY_REQUEST]) {
85+
request[POLLY_REQUEST].promise.resolve(request.response());
86+
delete request[POLLY_REQUEST];
87+
}
8088
},
8189
requestfailed: request => {
8290
// Reject the passthrough promise with the error object if it exists
83-
request[PASSTHROUGH_PROMISE] &&
91+
if (request[PASSTHROUGH_PROMISE]) {
8492
request[PASSTHROUGH_PROMISE].reject(request.failure());
93+
delete request[PASSTHROUGH_PROMISE];
94+
}
8595

86-
delete request[PASSTHROUGH_PROMISE];
96+
// Reject the deferred pollyRequest promise with the error object if it exists
97+
if (request[POLLY_REQUEST]) {
98+
request[POLLY_REQUEST].promise.reject(request.failure());
99+
delete request[POLLY_REQUEST];
100+
}
87101
},
88102
close: () => this[LISTENERS].delete(page)
89103
});
90104

91105
this._callListenersWith('prependListener', page);
92106
}
93107

108+
onRequest(pollyRequest) {
109+
const [request] = pollyRequest.requestArguments;
110+
111+
/*
112+
Create an access point to the `pollyRequest` so it can be accessed from
113+
the emitted page events
114+
*/
115+
request[POLLY_REQUEST] = pollyRequest;
116+
}
117+
118+
/**
119+
* Override the onRequestFinished logic as it doesn't apply to this adapter.
120+
* Instead, that logic is re-implemented via the `requestfinished` page
121+
* event.
122+
*
123+
* @override
124+
*/
125+
onRequestFinished() {}
126+
127+
/**
128+
* Abort the request on failure. The parent `onRequestFailed` has been
129+
* re-implemented via the `requestfailed` page event.
130+
*
131+
* @override
132+
*/
133+
async onRequestFailed(pollyRequest) {
134+
const [request] = pollyRequest.requestArguments;
135+
136+
await request.abort();
137+
}
138+
94139
async onRecord(pollyRequest) {
95140
await this.passthroughRequest(pollyRequest);
96141
await this.persister.recordRequest(pollyRequest);

packages/@pollyjs/adapter-puppeteer/tests/integration/adapter-test.js

+36
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,40 @@ describe('Integration | Puppeteer Adapter', function() {
6464
setupPolly.afterEach();
6565

6666
adapterTests();
67+
68+
it('should have resolved requests after flushing', async function() {
69+
// Timeout after 500ms since we could have a hanging while loop
70+
this.timeout(500);
71+
72+
const { server } = this.polly;
73+
const requests = [];
74+
const resolved = [];
75+
let i = 1;
76+
77+
server
78+
.get(this.recordUrl())
79+
.intercept(async (req, res) => {
80+
await server.timeout(5);
81+
res.sendStatus(200);
82+
})
83+
.on('request', req => requests.push(req));
84+
85+
this.page.on('requestfinished', () => resolved.push(i++));
86+
87+
this.fetchRecord();
88+
this.fetchRecord();
89+
this.fetchRecord();
90+
91+
// Since it takes time for Puppeteer to execute the request in the browser's
92+
// context, we have to wait until the requests have been made.
93+
while (requests.length !== 3) {
94+
await server.timeout(5);
95+
}
96+
97+
await this.polly.flush();
98+
99+
expect(requests).to.have.lengthOf(3);
100+
requests.forEach(request => expect(request.didRespond).to.be.true);
101+
expect(resolved).to.have.members([1, 2, 3]);
102+
});
67103
});

packages/@pollyjs/adapter/src/index.js

+59-6
Original file line numberDiff line numberDiff line change
@@ -87,17 +87,28 @@ export default class Adapter {
8787
}
8888
}
8989

90-
handleRequest() {
91-
return this[REQUEST_HANDLER](...arguments);
90+
async handleRequest(request) {
91+
const pollyRequest = this.polly.registerRequest(request);
92+
93+
await pollyRequest.setup();
94+
await this.onRequest(pollyRequest);
95+
96+
try {
97+
const result = await this[REQUEST_HANDLER](pollyRequest);
98+
99+
await this.onRequestFinished(pollyRequest, result);
100+
101+
return result;
102+
} catch (error) {
103+
await this.onRequestFailed(pollyRequest, error);
104+
throw error;
105+
}
92106
}
93107

94-
async [REQUEST_HANDLER](request) {
108+
async [REQUEST_HANDLER](pollyRequest) {
95109
const { mode } = this.polly;
96-
const pollyRequest = this.polly.registerRequest(request);
97110
let interceptor;
98111

99-
await pollyRequest.setup();
100-
101112
if (pollyRequest.shouldIntercept) {
102113
interceptor = new Interceptor();
103114
const response = await this.intercept(pollyRequest, interceptor);
@@ -203,6 +214,7 @@ export default class Adapter {
203214
);
204215
}
205216

217+
/* Required Hooks */
206218
onConnect() {
207219
this.assert('Must implement the `onConnect` hook.', false);
208220
}
@@ -211,19 +223,60 @@ export default class Adapter {
211223
this.assert('Must implement the `onDisconnect` hook.', false);
212224
}
213225

226+
/**
227+
* @param {PollyRequest} pollyRequest
228+
* @returns {*}
229+
*/
214230
onRecord() {
215231
this.assert('Must implement the `onRecord` hook.', false);
216232
}
217233

234+
/**
235+
* @param {PollyRequest} pollyRequest
236+
* @param {Object} normalizedResponse The normalized response generated from the recording entry
237+
* @param {Object} recordingEntry The entire recording entry
238+
* @returns {*}
239+
*/
218240
onReplay() {
219241
this.assert('Must implement the `onReplay` hook.', false);
220242
}
221243

244+
/**
245+
* @param {PollyRequest} pollyRequest
246+
* @param {PollyResponse} response
247+
* @returns {*}
248+
*/
222249
onIntercept() {
223250
this.assert('Must implement the `onIntercept` hook.', false);
224251
}
225252

253+
/**
254+
* @param {PollyRequest} pollyRequest
255+
* @returns {*}
256+
*/
226257
onPassthrough() {
227258
this.assert('Must implement the `onPassthrough` hook.', false);
228259
}
260+
261+
/* Other Hooks */
262+
/**
263+
* @param {PollyRequest} pollyRequest
264+
*/
265+
onRequest() {}
266+
267+
/**
268+
* @param {PollyRequest} pollyRequest
269+
* @param {*} result The returned result value from the request handler
270+
*/
271+
onRequestFinished(pollyRequest, result) {
272+
pollyRequest.promise.resolve(result);
273+
}
274+
275+
/**
276+
* @param {PollyRequest} pollyRequest
277+
* @param {Error} error
278+
*/
279+
onRequestFailed(pollyRequest, error) {
280+
pollyRequest.promise.reject(error);
281+
}
229282
}

packages/@pollyjs/core/src/-private/request.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import PollyResponse from './response';
55
import NormalizeRequest from '../utils/normalize-request';
66
import parseUrl from '../utils/parse-url';
77
import serializeRequestBody from '../utils/serialize-request-body';
8+
import DeferredPromise from '../utils/deferred-promise';
89
import isAbsoluteUrl from 'is-absolute-url';
910
import { assert, timestamp } from '@pollyjs/utils';
1011
import HTTPBase from './http-base';
@@ -29,6 +30,7 @@ export default class PollyRequest extends HTTPBase {
2930
this.recordingName = polly.recordingName;
3031
this.recordingId = polly.recordingId;
3132
this.requestArguments = freeze(request.requestArguments || []);
33+
this.promise = new DeferredPromise();
3234
this[POLLY] = polly;
3335

3436
/*
@@ -54,11 +56,7 @@ export default class PollyRequest extends HTTPBase {
5456
get absoluteUrl() {
5557
const { url } = this;
5658

57-
if (!isAbsoluteUrl(url)) {
58-
return new URL(url).href;
59-
}
60-
61-
return url;
59+
return isAbsoluteUrl(url) ? url : new URL(url).href;
6260
}
6361

6462
get protocol() {

packages/@pollyjs/core/src/polly.js

+10
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,16 @@ export default class Polly {
234234
}
235235
}
236236

237+
async flush() {
238+
const NOOP = () => {};
239+
240+
await Promise.all(
241+
// The NOOP is there to handle both a resolved and rejected promise
242+
// to ensure the promise resolves regardless of the outcome.
243+
this._requests.map(r => Promise.resolve(r.promise).then(NOOP, NOOP))
244+
);
245+
}
246+
237247
/**
238248
* @param {String|Function} nameOrFactory
239249
* @public
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Create a deferred promise with `resolve` and `reject` methods.
3+
*/
4+
export default class DeferredPromise extends Promise {
5+
constructor() {
6+
let resolve, reject;
7+
8+
super((_resolve, _reject) => {
9+
resolve = _resolve;
10+
reject = _reject;
11+
});
12+
13+
this.resolve = resolve;
14+
this.reject = reject;
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import DeferredPromise from '../../../src/utils/deferred-promise';
2+
3+
describe('Unit | Utils | DeferredPromise', function() {
4+
it('should exist', function() {
5+
expect(DeferredPromise).to.be.a('function');
6+
expect(new DeferredPromise().resolve).to.be.a('function');
7+
expect(new DeferredPromise().reject).to.be.a('function');
8+
});
9+
10+
it('should resolve when calling .resolve()', async function() {
11+
const promise = new DeferredPromise();
12+
13+
promise.resolve(42);
14+
expect(await promise).to.equal(42);
15+
});
16+
17+
it('should reject when calling .reject()', async function() {
18+
const promise = new DeferredPromise();
19+
20+
promise.reject(new Error('42'));
21+
22+
try {
23+
await promise;
24+
} catch (error) {
25+
expect(error).to.be.an('error');
26+
expect(error.message).to.equal('42');
27+
}
28+
});
29+
});

testem.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module.exports = {
1515
'./tests/*',
1616
'./tests/!(recordings)/**/*',
1717
'./packages/@pollyjs/*/src/**/*',
18-
'./packages/@pollyjs/*/tests/*/*'
18+
'./packages/@pollyjs/*/tests/**/*'
1919
],
2020
serve_files: ['./packages/@pollyjs/*/build/browser/*.js'],
2121
browser_args: {
@@ -33,12 +33,11 @@ module.exports = {
3333
middleware: [attachMiddleware],
3434
launchers: {
3535
Node: {
36-
command:
37-
'mocha ./packages/@pollyjs/*/build/node/*.js --ui bdd --reporter tap --require tests/node-setup.js',
36+
command: 'yarn test:node --reporter tap',
3837
protocol: 'tap'
3938
},
4039
Jest: {
41-
command: 'jest',
40+
command: 'yarn test:jest',
4241
protocol: 'tap'
4342
},
4443
Ember: {

0 commit comments

Comments
 (0)