Skip to content

Commit

Permalink
feat: Simplify adapter implementation (#154)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Changes to the base adapter implementation and external facing API
  • Loading branch information
Thomas Scholtes authored and offirgolan committed Jan 21, 2019
1 parent 0587aa8 commit 12c8601
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 158 deletions.
80 changes: 78 additions & 2 deletions docs/adapters/custom.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,87 @@ class CustomAdapter extends Adapter {
onDisconnect() {
/* Do something when the adapter is disconnected from */
}

async passthroughRequest(pollyRequest) {
/* Do something when the adapter is connect to */
}

/* optional */
async respondToRequest(pollyRequest) {
const { statusCode, body, headers } = pollyRequest.response
/* Deliver the response to the user */
}
}
```

For better usage examples, please refer to the source code for
the [Fetch](https://github.com/Netflix/pollyjs/blob/master/packages/@pollyjs/adapter-fetch/src/index.js) & [XHR](https://github.com/Netflix/pollyjs/blob/master/packages/%40pollyjs/adapter-xhr/src/index.js) adapters.
The `Adapter` class provides the `handleRequest()` method which can be
called from `onConnect`. It accepts request parameters and returns a
PollyRequest object with a `response` property.

The `passthroughRequest` method takes a PollyRequest object, makes a real HTTP
request and returns the response as a `{ statusCode, headers, body }` object,
where `body` is a string.

The `respondToRequest()` method makes sure that the response has been delivered
to the user. `pollyjs.flush()` will wait for all `respondToRequests()` calls to
finish. You can omit the implementation of this method if no asynchronous
delivery is required.

### Simple Fetch adapter example

The following is a simple example of implementing an adapter for `fetch`. For
full examples, please refer to the source code for the
[Fetch](https://github.com/Netflix/pollyjs/blob/master/packages/@pollyjs/adapter-fetch/src/index.js)
& [XHR](https://github.com/Netflix/pollyjs/blob/master/packages/%40pollyjs/adapter-xhr/src/index.js)
adapters.


```js
class FetchAdapter extends Adapter {
static get name() {
return 'fetch';
}

onConnect() {
this.originalFetch = window.fetch;

window.fetch = async (url, options = {}) => {
const { repsonse } = await this.handleRequest({
url,
method: options.method,
headers: options.headers,
body: options.body,
});

return new Response(response.body, {
status: response.statusCode,
headers: response.headers
});
};
}

onDisconnect() {
window.fetch = this.originalFetch
}

async passthroughRequest(pollyRequest) {
const response = await this.originalFetch([
pollyRequest.url,
{
method: pollyRequest.method,
headers: pollyRequest.headers,
body: pollyRequest.body
}
]);

return {
statusCode: response.status,
headers: serializeHeaders(response.headers),
body: await response.text()
};
}
}
```

## Extending from an Existing Adapter

Expand Down
76 changes: 29 additions & 47 deletions packages/@pollyjs/adapter-fetch/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,18 @@ export default class FetchAdapter extends Adapter {

this.native = context.fetch;

context.fetch = (url, options = {}) =>
this.handleRequest({
context.fetch = async (url, options = {}) => {
const pollyRequest = await this.handleRequest({
url,
method: options.method || 'GET',
headers: serializeHeaders(options.headers),
body: options.body,
requestArguments: [url, options]
requestArguments: { options }
});

return makeResponse(context.Response, pollyRequest);
};

defineProperty(context.fetch, IS_STUBBED, { value: true });
}

Expand All @@ -52,20 +55,8 @@ export default class FetchAdapter extends Adapter {
this.native = null;
}

async onRecord(pollyRequest) {
const response = await this.onPassthrough(pollyRequest);

await this.persister.recordRequest(pollyRequest);

return response;
}

onReplay(pollyRequest, { statusCode, headers, body }) {
return this.respond(pollyRequest, statusCode, headers, body);
}

async onPassthrough(pollyRequest) {
const [, options] = pollyRequest.requestArguments;
async passthroughRequest(pollyRequest) {
const { options } = pollyRequest.requestArguments;

const response = await this.native.apply(global, [
pollyRequest.url,
Expand All @@ -77,39 +68,30 @@ export default class FetchAdapter extends Adapter {
}
]);

return this.respond(
pollyRequest,
response.status,
serializeHeaders(response.headers),
await response.text()
);
}

onIntercept(pollyRequest, { statusCode, headers, body }) {
return this.respond(pollyRequest, statusCode, headers, body);
return {
statusCode: response.status,
headers: serializeHeaders(response.headers),
body: await response.text()
};
}
}

async respond(pollyRequest, status, headers, body) {
const { Response } = this.options.context;

await pollyRequest.respond(status, headers, body);

const { absoluteUrl, response } = pollyRequest;
const { statusCode } = response;
function makeResponse(Response, pollyRequest) {
const { absoluteUrl, response } = pollyRequest;
const { statusCode } = response;

const responseBody =
statusCode === 204 && response.body === '' ? null : response.body;
const fetchResponse = new Response(responseBody, {
status: statusCode,
headers: response.headers
});
const responseBody =
statusCode === 204 && response.body === '' ? null : response.body;
const fetchResponse = new Response(responseBody, {
status: statusCode,
headers: response.headers
});

/*
Response does not allow `url` to be set manually (either via the
constructor or assignment) so force the url property via `defineProperty`.
*/
defineProperty(fetchResponse, 'url', { value: absoluteUrl });
/*
Response does not allow `url` to be set manually (either via the
constructor or assignment) so force the url property via `defineProperty`.
*/
defineProperty(fetchResponse, 'url', { value: absoluteUrl });

return fetchResponse;
}
return fetchResponse;
}
27 changes: 2 additions & 25 deletions packages/@pollyjs/adapter-node-http/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,36 +35,13 @@ export default class HttpAdapter extends Adapter {
}
}

async onRecord(pollyRequest) {
await this.passthroughRequest(pollyRequest);
await this.persister.recordRequest(pollyRequest);
await this.respond(pollyRequest);
}

async onReplay(pollyRequest, { statusCode, headers, body }) {
await pollyRequest.respond(statusCode, headers, body);

await this.respond(pollyRequest);
}

async onPassthrough(pollyRequest) {
await this.passthroughRequest(pollyRequest);
await this.respond(pollyRequest);
}

async onIntercept(pollyRequest, { statusCode, headers, body }) {
await pollyRequest.respond(statusCode, headers, body);
await this.respond(pollyRequest);
}

async passthroughRequest(pollyRequest) {
const [transportWrapper] = pollyRequest.requestArguments;
const res = await transportWrapper.passthrough(pollyRequest);

await pollyRequest.respond(res.statusCode, res.headers, res.body);
return transportWrapper.passthrough(pollyRequest);
}

async respond(pollyRequest) {
async respondToRequest(pollyRequest) {
const [transportWrapper] = pollyRequest.requestArguments;

return transportWrapper.respond(pollyRequest);
Expand Down
50 changes: 15 additions & 35 deletions packages/@pollyjs/adapter-puppeteer/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default class PuppeteerAdapter extends Adapter {
const { requestResourceTypes } = this.options;

this[LISTENERS].set(page, {
request: request => {
request: async request => {
if (requestResourceTypes.includes(request.resourceType())) {
const url = request.url();
const method = request.method();
Expand Down Expand Up @@ -89,13 +89,21 @@ export default class PuppeteerAdapter extends Adapter {
// Do not intercept preflight requests
request.continue();
} else {
this.handleRequest({
const pollyRequest = await this.handleRequest({
headers,
url,
method,
body: request.postData(),
requestArguments: [request]
});

const { response } = pollyRequest;

request.respond({
status: response.statusCode,
headers: response.headers,
body: response.body
});
}
} else {
request.continue();
Expand Down Expand Up @@ -170,34 +178,6 @@ export default class PuppeteerAdapter extends Adapter {
await request.abort();
}

async onRecord(pollyRequest) {
await this.passthroughRequest(pollyRequest);
await this.persister.recordRequest(pollyRequest);
this.respondToPuppeteerRequest(pollyRequest);
}

async onReplay(pollyRequest, { statusCode, headers, body }) {
await pollyRequest.respond(statusCode, headers, body);
this.respondToPuppeteerRequest(pollyRequest);
}

async onPassthrough(pollyRequest) {
await this.passthroughRequest(pollyRequest);
this.respondToPuppeteerRequest(pollyRequest);
}

async onIntercept(pollyRequest, { statusCode, headers, body }) {
await pollyRequest.respond(statusCode, headers, body);
this.respondToPuppeteerRequest(pollyRequest);
}

respondToPuppeteerRequest(pollyRequest) {
const [request] = pollyRequest.requestArguments;
const { statusCode: status, headers, body } = pollyRequest.response;

request.respond({ status, headers, body });
}

async passthroughRequest(pollyRequest) {
const { page } = this.options;
const { id, order, url, method, headers, body } = pollyRequest;
Expand Down Expand Up @@ -227,11 +207,11 @@ export default class PuppeteerAdapter extends Adapter {
);
});

return pollyRequest.respond(
response.status(),
response.headers(),
await response.text()
);
return {
statusCode: response.status(),
headers: response.headers(),
body: await response.text()
};
} catch (error) {
console.error(error);
throw error;
Expand Down
49 changes: 15 additions & 34 deletions packages/@pollyjs/adapter-xhr/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,16 @@ export default class XHRAdapter extends Adapter {

this.xhr.onCreate = xhr => {
xhr[SEND] = xhr.send;
xhr.send = body =>
xhr.send = async body => {
xhr[SEND](body);
this.handleRequest({
url: xhr.url,
method: xhr.method || 'GET',
headers: xhr.requestHeaders,
requestArguments: [xhr, body],
requestArguments: { xhr },
body
});
};
};

global.XMLHttpRequest[IS_STUBBED] = true;
Expand All @@ -42,37 +44,15 @@ export default class XHRAdapter extends Adapter {
this.xhr.restore();
}

async onRecord(pollyRequest) {
await this.passthroughRequest(pollyRequest);
await this.persister.recordRequest(pollyRequest);
this.respondToXhr(pollyRequest);
}

async onReplay(pollyRequest, { statusCode, headers, body }) {
await pollyRequest.respond(statusCode, headers, body);
this.respondToXhr(pollyRequest);
}
respondToRequest(pollyRequest) {
const { xhr } = pollyRequest.requestArguments;
const { response } = pollyRequest;

async onPassthrough(pollyRequest) {
await this.passthroughRequest(pollyRequest);
this.respondToXhr(pollyRequest);
}

async onIntercept(pollyRequest, { statusCode, headers, body }) {
await pollyRequest.respond(statusCode, headers, body);
this.respondToXhr(pollyRequest);
}

respondToXhr(pollyRequest) {
const [fakeXhr] = pollyRequest.requestArguments;
const { body, response } = pollyRequest;

fakeXhr[SEND](body);
fakeXhr.respond(response.statusCode, response.headers, response.body);
xhr.respond(response.statusCode, response.headers, response.body);
}

async passthroughRequest(pollyRequest) {
const [fakeXhr] = pollyRequest.requestArguments;
const fakeXhr = pollyRequest.requestArguments.xhr;

const xhr = new this.native();

Expand All @@ -96,10 +76,11 @@ export default class XHRAdapter extends Adapter {
}

await resolveXhr(xhr, pollyRequest.body);
await pollyRequest.respond(
xhr.status,
serializeResponseHeaders(xhr.getAllResponseHeaders()),
xhr.responseText
);

return {
statusCode: xhr.status,
headers: serializeResponseHeaders(xhr.getAllResponseHeaders()),
body: xhr.responseText
};
}
}
Loading

0 comments on commit 12c8601

Please sign in to comment.