Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filterCalls(), testCalls() #83

Closed
wants to merge 8 commits into from
26 changes: 18 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,23 +41,33 @@ Clears all data recorded for `fetch()`'s calls

*Note that `restore()`, `reMock()` and `reset()` are all bound to fetchMock, and can be used directly as callbacks e.g. `afterEach(fetchMock.restore)` will work just fine. There is no need for `afterEach(function () {fetchMock.restore()})`*

**For the methods below `matcher`, if given, should be either the name of a route (see advanced usage below) or equal to `matcher.toString()` for any unnamed route**
**For the methods below `name`, if given, should be either the name of a route (see advanced usage below) or equal to `matcher.toString()` for any unnamed route**

#### `calls(matcher)`
Returns an object `{matched: [], unmatched: []}` containing arrays of all calls to fetch, grouped by whether fetch-mock matched them or not. If `matcher` is specified then only calls to fetch matching that route are returned.
#### `calls(name)`
Returns an object `{matched: [], unmatched: []}` containing arrays of all calls to fetch, grouped by whether fetch-mock matched them or not. If `name` is specified then only calls to fetch matching that route are returned.

#### `called(matcher)`
Returns a Boolean indicating whether fetch was called and a route was matched. If `matcher` is specified it only returns `true` if that particular route was matched.
#### `called(name)`
Returns a Boolean indicating whether fetch was called and a route was matched. If `name` is specified it only returns `true` if that particular route was matched.

#### `lastCall(matcher)`
#### `lastCall(name)`
Returns the arguments for the last matched call to fetch

#### `lastUrl(matcher)`
#### `lastUrl(name)`
Returns the url for the last matched call to fetch

#### `lastOptions(matcher)`
#### `lastOptions(name)`
Returns the options for the last matched call to fetch

**In the following methods, `matcher` can be a `string`, `RegExp` or `Function(url, opts)` and will be used to search through _all_ calls, regardless of whether they previously matched any configured routes**

#### `filterCalls(matcher)`
Returns an object `{routed: [], unrouted: []}` containing arrays of all matching calls to fetch, grouped by whether they matched any routes or not. `routed` and `unrouted` here are analogous to `matched` and `unmatched` as returned by `calls(name)`.

#### `testCalls(matcher)`
Returns a Boolean indicating whether fetch was called with a URL matching `matcher`.



##### Example

```js
Expand Down
92 changes: 71 additions & 21 deletions src/fetch-mock.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,55 @@ function normalizeRequest (url, options) {
}
}

/**
* compileUrlMatcher
* Compiles a URL matching function.
* @param {String|RegExp|Function(String, Object=):Boolean} matcher
* @return {Function(String, Object=):Boolean}
*/
function compileUrlMatcher (matcher) {
if (typeof matcher === 'function') {
return matcher;
}
else if (typeof matcher === 'string') {

if (matcher.indexOf('^') === 0) {
const expectedUrl = matcher.substr(1);
return function (url) {
return url.indexOf(expectedUrl) === 0;
};
} else {
const expectedUrl = matcher;
return function (url) {
return url === expectedUrl;
};
}
} else if (matcher instanceof RegExp) {
const urlRX = matcher;
return function (url) {
return urlRX.test(url);
};
}
else {
throw new Error('URL matcher must be a function, string, or RegExp');
}
}

/**
* compileUserUrlMatcher
* Compiles a URL matching function that also normalizes Request objects
* @param {String|RegExp|Function(String, Object=):Boolean} matcher
* @return {Function(String, Object=):Boolean}
*/
function compileUserUrlMatcher (matcher) {
matcher = compileUrlMatcher(matcher);
return function (url, options) {
const req = normalizeRequest(url, options);
return matcher(req.url, options);
};
}


/**
* compileRoute
* Given a route configuration object, validates the object structure and compiles
Expand Down Expand Up @@ -116,27 +165,7 @@ function compileRoute (route) {
return !expectedMethod || expectedMethod === (method ? method.toLowerCase() : 'get');
};

let matchUrl;

if (typeof route.matcher === 'string') {

if (route.matcher.indexOf('^') === 0) {
const expectedUrl = route.matcher.substr(1);
matchUrl = function (url) {
return url.indexOf(expectedUrl) === 0;
};
} else {
const expectedUrl = route.matcher;
matchUrl = function (url) {
return url === expectedUrl;
};
}
} else if (route.matcher instanceof RegExp) {
const urlRX = route.matcher;
matchUrl = function (url) {
return urlRX.test(url);
};
}
let matchUrl = compileUrlMatcher(route.matcher);

route.matcher = function (url, options) {
const req = normalizeRequest(url, options);
Expand Down Expand Up @@ -386,6 +415,27 @@ class FetchMock {
}
return !!(this._calls[name] && this._calls[name].length);
}

/**
* filterCalls
* Returns call history filtered by matcher. See README
*/
filterCalls (matcher) {
matcher = compileUserUrlMatcher(matcher);
return {
routed: this._matchedCalls.filter(call => matcher(call[0], call[1])),
unrouted: this._unmatchedCalls.filter(call => matcher(call[0], call[1]))
};
}

/**
* testCalls
* Returns whether fetch has been called with a matching URL. See README
*/
testCalls (matcher) {
matcher = compileUserUrlMatcher(matcher);
return [this._matchedCalls, this._unmatchedCalls].some(calls => calls.some(call => matcher(call[0], call[1])));
}
}

module.exports = FetchMock;
157 changes: 156 additions & 1 deletion test/spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,9 @@ module.exports = function (fetchMock, theGlobal, Request) {
expect(fetchMock.calls('route2').length).to.equal(1);
expect(fetchMock.calls().matched.length).to.equal(2);
expect(fetchMock.calls().unmatched.length).to.equal(1);
expect(fetchMock.testCalls('^http://it')).to.be.true;
expect(fetchMock.filterCalls('^http://it').routed.length).to.equal(2);
expect(fetchMock.filterCalls('^http://it').unrouted.length).to.equal(1);
});
});

Expand All @@ -429,6 +432,9 @@ module.exports = function (fetchMock, theGlobal, Request) {
expect(fetchMock.calls('route1').length).to.equal(1);
expect(fetchMock.calls().matched.length).to.equal(1);
expect(fetchMock.calls('route2').length).to.equal(0);
expect(fetchMock.testCalls('^http://it')).to.be.true;
expect(fetchMock.filterCalls('^http://it').routed.length).to.equal(1);
expect(fetchMock.filterCalls('^http://it').unrouted.length).to.equal(0);
});
});

Expand All @@ -450,7 +456,7 @@ module.exports = function (fetchMock, theGlobal, Request) {
});
});

it('have helpers to retrieve paramaters pf last call', function () {
it('have helpers to retrieve parameters of last call', function () {
fetchMock.mock({
routes: {
name: 'route',
Expand Down Expand Up @@ -514,7 +520,156 @@ module.exports = function (fetchMock, theGlobal, Request) {
});

});
describe('call matching', function () {
it('match exact strings', function () {
fetchMock.mock({
routes: {
name: 'route',
matcher: 'http://it.at.there/',
response: 'ok'
}
});
return Promise.all([fetch('http://it.at.there/'), fetch('http://it.at.thereabouts')])
.then(function () {
expect(fetchMock.testCalls('route')).to.be.false;
expect(fetchMock.testCalls('http://it.at.there/')).to.be.true;
expect(fetchMock.testCalls('http://it.at.thereabouts')).to.be.true;
expect(fetchMock.filterCalls('route').routed.length).to.equal(0);
expect(fetchMock.filterCalls('http://it.at.there/').routed.length).to.equal(1);
expect(fetchMock.filterCalls('http://it.at.thereabouts').unrouted.length).to.equal(1);
});
});

it('match when relative url', function () {
fetchMock.mock({
routes: {
name: 'route',
matcher: '/it.at.there/',
method: 'POST',
response: 'ok'
}
});
return fetch('/it.at.there/', {method: 'POST'})
.then(function () {
expect(fetchMock.testCalls('route')).to.be.false;
expect(fetchMock.testCalls('/it.at.there/')).to.be.true;
expect(fetchMock.filterCalls('route').routed.length).to.equal(0);
expect(fetchMock.filterCalls('/it.at.there/').routed.length).to.equal(1);
});
});

it('match when Request instance', function () {
fetchMock.mock({
routes: {
name: 'route',
matcher: 'http://it.at.there/',
method: 'POST',
response: 'ok'
}
});
return fetch(new Request('http://it.at.there/', {method: 'POST'}))
.then(function () {
expect(fetchMock.testCalls('route')).to.be.false;
expect(fetchMock.testCalls('http://it.at.there/')).to.be.true;
expect(fetchMock.filterCalls('route').routed.length).to.equal(0);
expect(fetchMock.filterCalls('http://it.at.there/').routed.length).to.equal(1);
});
});

it('match strings starting with a string', function () {
fetchMock.mock({
routes: {
name: 'route',
matcher: '^http://it.at.there',
response: 'ok'
}
});
return Promise.all([
fetch('http://it.at.there'),
fetch('http://it.at.thereabouts'),
fetch('http://it.at.hereabouts')]
)
.then(function () {
expect(fetchMock.testCalls('route')).to.be.false;
expect(fetchMock.testCalls('http://it.at.there')).to.be.true;
expect(fetchMock.testCalls('http://it.at.thereabouts')).to.be.true;
expect(fetchMock.testCalls('http://it.at.hereabouts')).to.be.true;
expect(fetchMock.testCalls('^http://it')).to.be.true;
expect(fetchMock.testCalls('^http://it.at.there')).to.be.true;
expect(fetchMock.testCalls('^http://it.at.therewego')).to.be.false;
expect(fetchMock.filterCalls('route').routed.length).to.equal(0);
expect(fetchMock.filterCalls('http://it.at.there').routed.length).to.equal(1);
expect(fetchMock.filterCalls('http://it.at.thereabouts').routed.length).to.equal(1);
expect(fetchMock.filterCalls('http://it.at.hereabouts').unrouted.length).to.equal(1);
expect(fetchMock.filterCalls('^http://it').routed.length).to.equal(2);
expect(fetchMock.filterCalls('^http://it').unrouted.length).to.equal(1);
expect(fetchMock.filterCalls('^http://it.at.there').routed.length).to.equal(2);
expect(fetchMock.filterCalls('^http://it.at.therewego').routed.length).to.equal(0);
});
});

it('match regular expressions', function () {
fetchMock.mock({
routes: {
name: 'route',
matcher: /http\:\/\/it\.at\.there\/\d+/,
response: 'ok'
}
});
return Promise.all([fetch('http://it.at.there/'), fetch('http://it.at.there/12345'), fetch('http://it.at.there/abcde')])
.then(function () {
expect(fetchMock.testCalls('route')).to.be.false;
expect(fetchMock.testCalls(/http\:\/\/it\.at\.there\/\d+/)).to.be.true;
expect(fetchMock.testCalls(/http\:\/\/it\.at\.there\/[a-e]+/)).to.be.true;
expect(fetchMock.testCalls(/http\:\/\/it\.at\.there\/[f-z]+/)).to.be.false;
expect(fetchMock.filterCalls('route').routed.length).to.equal(0);
expect(fetchMock.filterCalls(/http\:\/\/it\.at\.there\/\d+/).routed.length).to.equal(1);
expect(fetchMock.filterCalls(/http\:\/\/it\.at\.there\/[a-e]+/).routed.length).to.equal(0);
expect(fetchMock.filterCalls(/http\:\/\/it\.at\.there\/[a-e]+/).unrouted.length).to.equal(1);
expect(fetchMock.filterCalls(/http\:\/\/it\.at\.there\/[f-z]+/).routed.length).to.equal(0);
});
});

it('match using custom functions', function () {
fetchMock.mock({
routes: {
name: 'route',
matcher: function (url, opts) {
return url.indexOf('logged-in') > -1 && opts && opts.headers && opts.headers.authorized === true;
},
response: 'ok'
}
});
return Promise.all([
fetch('http://it.at.there/logged-in', {headers:{authorized: true}}),
fetch('http://it.at.there/12345', {headers:{authorized: true}}),
fetch('http://it.at.there/logged-in')
])
.then(function () {
expect(fetchMock.testCalls('route')).to.be.false;
expect(fetchMock.testCalls(function (url, opts) {
return url.indexOf('logged-in') > -1 && opts && opts.headers && opts.headers.authorized === true;
})).to.be.true;
expect(fetchMock.testCalls(function (url, opts) {
return url.indexOf('12345') > -1 && opts && opts.headers && opts.headers.authorized === true;
})).to.be.true;
expect(fetchMock.testCalls(function (url, opts) {
return url.indexOf('67890') > -1 && opts && opts.headers && opts.headers.authorized === true;
})).to.be.false;

expect(fetchMock.filterCalls('route').routed.length).to.equal(0);
expect(fetchMock.filterCalls(function (url, opts) {
return url.indexOf('logged-in') > -1 && opts && opts.headers && opts.headers.authorized === true;
}).routed.length).to.equal(1);
expect(fetchMock.filterCalls(function (url, opts) {
return url.indexOf('12345') > -1 && opts && opts.headers && opts.headers.authorized === true;
}).unrouted.length).to.equal(1);
expect(fetchMock.filterCalls(function (url, opts) {
return url.indexOf('67890') > -1 && opts && opts.headers && opts.headers.authorized === true;
}).routed.length).to.equal(0);
});
});
});
describe('responses', function () {

it('respond with a status', function () {
Expand Down