Skip to content

Commit

Permalink
Merge pull request #564 from wheresrhys/record-responses
Browse files Browse the repository at this point in the history
make response available to be inspected
  • Loading branch information
wheresrhys committed May 23, 2020
2 parents fe4e01d + 1cca5da commit 5446946
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 51 deletions.
2 changes: 1 addition & 1 deletion docs/_api-inspection/lastOptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ navTitle: .lastOptions()
position: 5
versionAdded: 4.0.0
description: |-
Returns the options for the call to `fetch` matching the given `filter` and `options`. If `fetch` was last called using a `Request` instance, a set of `options` inferred from the `Request` will be returned
Returns the options for the last call to `fetch` matching the given `filter` and `options`. If `fetch` was last called using a `Request` instance, a set of `options` inferred from the `Request` will be returned
---
14 changes: 14 additions & 0 deletions docs/_api-inspection/lastResponse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: .lastResponse(filter, options)
navTitle: .lastResponse()
position: 5.5
versionAdded: 9.10.0
description: |-
Returns the `Response` for the last call to `fetch` matching the given `filter` and `options`.
If `.lastResponse()` is called before fetch has been resolved then it will return `undefined`
{: .warning}
To obtain json/text responses await the `.json()/.text()` methods of the response
{: .info}
---
115 changes: 67 additions & 48 deletions src/lib/fetch-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,42 +61,45 @@ const resolve = async (
}
};

FetchMock.fetchHandler = function (url, options, request) {
FetchMock.needsAsyncBodyExtraction = function ({ request }) {
return request && this.routes.some(({ usesBody }) => usesBody);
};

FetchMock.fetchHandler = function (url, options) {
setDebugPhase('handle');
const debug = getDebug('fetchHandler()');
debug('fetch called with:', url, options);

const normalizedRequest = requestUtils.normalizeRequest(
url,
options,
this.config.Request
);

({ url, options, request } = normalizedRequest);

const { signal } = normalizedRequest;

debug('Request normalised');
debug(' url', url);
debug(' options', options);
debug(' request', request);
debug(' signal', signal);
debug(' url', normalizedRequest.url);
debug(' options', normalizedRequest.options);
debug(' request', normalizedRequest.request);
debug(' signal', normalizedRequest.signal);

if (request && this.routes.some(({ usesBody }) => usesBody)) {
if (this.needsAsyncBodyExtraction(normalizedRequest)) {
debug(
'Need to wait for Body to be streamed before calling router: switching to async mode'
);
return this._asyncFetchHandler(url, options, request, signal);
return this._extractBodyThenHandle(normalizedRequest);
}
return this._fetchHandler(url, options, request, signal);
return this._fetchHandler(normalizedRequest);
};

FetchMock._asyncFetchHandler = async function (url, options, request, signal) {
options.body = await options.body;
return this._fetchHandler(url, options, request, signal);
FetchMock._extractBodyThenHandle = async function (normalizedRequest) {
normalizedRequest.options.body = await normalizedRequest.options.body;
return this._fetchHandler(normalizedRequest);
};

FetchMock._fetchHandler = function (url, options, request, signal) {
const route = this.executeRouter(url, options, request);
FetchMock._fetchHandler = function ({ url, options, request, signal }) {
const { route, callLog } = this.executeRouter(url, options, request);

this.recordCall(callLog);

// this is used to power the .flush() method
let done;
Expand All @@ -109,7 +112,9 @@ FetchMock._fetchHandler = function (url, options, request, signal) {
debug('signal exists - enabling fetch abort');
const abort = () => {
debug('aborting fetch');
// note that DOMException is not available in node.js; even node-fetch uses a custom error class: https://github.com/bitinn/node-fetch/blob/master/src/abort-error.js
// note that DOMException is not available in node.js;
// even node-fetch uses a custom error class:
// https://github.com/bitinn/node-fetch/blob/master/src/abort-error.js
rej(
typeof DOMException !== 'undefined'
? new DOMException('The operation was aborted.', 'AbortError')
Expand All @@ -124,7 +129,7 @@ FetchMock._fetchHandler = function (url, options, request, signal) {
signal.addEventListener('abort', abort);
}

this.generateResponse(route, url, options, request)
this.generateResponse({ route, url, options, request, callLog })
.then(res, rej)
.then(done, done)
.then(() => {
Expand All @@ -137,30 +142,44 @@ FetchMock.fetchHandler.isMock = true;

FetchMock.executeRouter = function (url, options, request) {
const debug = getDebug('executeRouter()');
const callLog = { url, options, request, isUnmatched: true };
debug(`Attempting to match request to a route`);
if (this.getOption('fallbackToNetwork') === 'always') {
debug(
' Configured with fallbackToNetwork=always - passing through to fetch'
);
return { response: this.getNativeFetch(), responseIsFetch: true };
return {
route: { response: this.getNativeFetch(), responseIsFetch: true },
// BUG - this callLog never used to get sent. Discovered the bug
// but can't fix outside a major release as it will potentially
// cause too much disruption
//
// callLog,
};
}

const match = this.router(url, options, request);
const route = this.router(url, options, request);

if (match) {
if (route) {
debug(' Matching route found');
return match;
return {
route,
callLog: {
url,
options,
request,
identifier: route.identifier,
},
};
}

if (this.getOption('warnOnFallback')) {
console.warn(`Unmatched ${(options && options.method) || 'GET'} to ${url}`); // eslint-disable-line
}

this.push({ url, options, request, isUnmatched: true });

if (this.fallbackResponse) {
debug(' No matching route found - using fallbackResponse');
return { response: this.fallbackResponse };
return { route: { response: this.fallbackResponse }, callLog };
}

if (!this.getOption('fallbackToNetwork')) {
Expand All @@ -172,10 +191,19 @@ FetchMock.executeRouter = function (url, options, request) {
}

debug(' Configured to fallbackToNetwork - passing through to fetch');
return { response: this.getNativeFetch(), responseIsFetch: true };
return {
route: { response: this.getNativeFetch(), responseIsFetch: true },
callLog,
};
};

FetchMock.generateResponse = async function (route, url, options, request) {
FetchMock.generateResponse = async function ({
route,
url,
options,
request,
callLog = {},
}) {
const debug = getDebug('generateResponse()');
const response = await resolve(route, url, options, request);

Expand All @@ -189,16 +217,21 @@ FetchMock.generateResponse = async function (route, url, options, request) {
// If the response is a pre-made Response, respond with it
if (this.config.Response.prototype.isPrototypeOf(response)) {
debug('response is already a Response instance - returning it');
callLog.response = response;
return response;
}

// finally, if we need to convert config into a response, we do it
return responseBuilder({
const finalResponse = responseBuilder({
url,
responseConfig: response,
fetchMock: this,
route,
});

callLog.response = finalResponse;

return finalResponse;
};

FetchMock.router = function (url, options, request) {
Expand All @@ -208,12 +241,6 @@ FetchMock.router = function (url, options, request) {
});

if (route) {
this.push({
url,
options,
request,
identifier: route.identifier,
});
return route;
}
};
Expand All @@ -228,19 +255,11 @@ FetchMock.getNativeFetch = function () {
return func;
};

FetchMock.push = function ({ url, options, request, isUnmatched, identifier }) {
debug('Recording fetch call', {
url,
options,
request,
isUnmatched,
identifier,
});
const args = [url, options];
args.request = request;
args.identifier = identifier;
args.isUnmatched = isUnmatched;
this._calls.push(args);
FetchMock.recordCall = function (obj) {
debug('Recording fetch call', obj);
if (obj) {
this._calls.push(obj);
}
};

module.exports = FetchMock;
24 changes: 22 additions & 2 deletions src/lib/inspecting.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ const filterCallsWithMatcher = function (matcher, options = {}, calls) {
matcher = this.generateMatcher(
this.sanitizeRoute(Object.assign({ matcher }, options))
);
return calls.filter(([url, options]) => matcher(normalizeUrl(url), options));
return calls.filter(({ url, options }) =>
matcher(normalizeUrl(url), options)
);
};

const formatDebug = (func) => {
Expand All @@ -20,6 +22,19 @@ const formatDebug = (func) => {
};
};

const callObjToArray = (obj) => {
if (!obj) {
return undefined;
}
const { url, options, request, identifier, isUnmatched, response } = obj;
const arr = [url, options];
arr.request = request;
arr.identifier = identifier;
arr.isUnmatched = isUnmatched;
arr.response = response;
return arr;
};

FetchMock.filterCalls = function (nameOrMatcher, options) {
debug('Filtering fetch calls');
let calls = this._calls;
Expand Down Expand Up @@ -62,7 +77,7 @@ FetchMock.filterCalls = function (nameOrMatcher, options) {
calls = filterCallsWithMatcher.call(this, matcher, options, calls);
}
debug(`Retrieved ${calls.length} calls`);
return calls;
return calls.map(callObjToArray);
};

FetchMock.calls = formatDebug(function (nameOrMatcher, options) {
Expand All @@ -85,6 +100,11 @@ FetchMock.lastOptions = formatDebug(function (nameOrMatcher, options) {
return (this.lastCall(nameOrMatcher, options) || [])[1];
});

FetchMock.lastResponse = formatDebug(function (nameOrMatcher, options) {
debug('retrieving respose of last matching call');
return (this.lastCall(nameOrMatcher, options) || []).response;
});

FetchMock.called = formatDebug(function (nameOrMatcher, options) {
debug('checking if matching call was made');
return Boolean(this.filterCalls(nameOrMatcher, options).length);
Expand Down
30 changes: 30 additions & 0 deletions test/specs/inspecting.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,34 @@ describe('inspecting', () => {
expect(options.signal).to.be.undefined;
});
});

describe('retrieving responses', () => {
it('exposes responses', async () => {
fm.once('*', 200).once('*', 201, { overwriteRoutes: false });

await fm.fetchHandler('http://a.com/');
await fm.fetchHandler('http://a.com/');
expect(fm.calls()[0].response.status).to.equal(200);
expect(fm.calls()[1].response.status).to.equal(201);
fm.restore();
});

it('exposes Responses', async () => {
fm.once('*', new fm.config.Response('blah'));

await fm.fetchHandler('http://a.com/');
expect(fm.calls()[0].response.status).to.equal(200);
expect(await fm.calls()[0].response.text()).to.equal('blah');
fm.restore();
});

it('has lastResponse shorthand', async () => {
fm.once('*', 200).once('*', 201, { overwriteRoutes: false });

await fm.fetchHandler('http://a.com/');
await fm.fetchHandler('http://a.com/');
expect(fm.lastResponse().status).to.equal(201);
fm.restore();
});
});
});
File renamed without changes.
File renamed without changes.

0 comments on commit 5446946

Please sign in to comment.