×

Welcome to TagMyCode

Please login or create account to add a snippet.
0
0
 
0
Language: Javascript
Posted by: Isaac Dettman
Added: Nov 4, 2016 9:55 PM
Views: 36
Javascript mock of the fetch method
  1. 'use strict';
  2.  
  3. const compileRoute = require('./compile-route');
  4.  
  5. class FetchMock {
  6.         constructor (opts) {
  7.                 this.config = {
  8.                         sendAsJson: true
  9.                 }
  10.                 this.Headers = opts.Headers;
  11.                 this.Request = opts.Request;
  12.                 this.Response = opts.Response;
  13.                 this.stream = opts.stream;
  14.                 this.global = opts.global;
  15.                 this.statusTextMap = opts.statusTextMap;
  16.                 this.routes = [];
  17.                 this._calls = {};
  18.                 this._matchedCalls = [];
  19.                 this._unmatchedCalls = [];
  20.                 this.fetchMock = this.fetchMock.bind(this);
  21.                 this.restore = this.restore.bind(this);
  22.                 this.reset = this.reset.bind(this);
  23.         }
  24.  
  25.         mock (matcher, response, options) {
  26.  
  27.                 let route;
  28.  
  29.                 // Handle the variety of parameters accepted by mock (see README)
  30.  
  31.                 // Old method matching signature
  32.                 if (options && /^[A-Z]+$/.test(response)) {
  33.                         throw new Error(`The API for method matching has changed.
  34.                                 Now use .get(), .post(), .put(), .delete() and .head() shorthand methods,
  35.                                 or pass in, e.g. {method: 'PATCH'} as a third paramter`);
  36.                 } else if (options) {
  37.                         route = Object.assign({
  38.                                 matcher,
  39.                                 response
  40.                         }, options);
  41.                 } else if (matcher && response) {
  42.                         route = {
  43.                                 matcher,
  44.                                 response
  45.                         }
  46.                 } else if (matcher && matcher.matcher) {
  47.                         route = matcher
  48.                 } else {
  49.                         throw new Error('Invalid parameters passed to fetch-mock')
  50.                 }
  51.  
  52.  
  53.                 this.addRoute(route);
  54.  
  55.                 return this._mock();
  56.         }
  57.  
  58.         once (matcher, response, options) {
  59.                 return this.mock(matcher, response, Object.assign({}, options, {times: 1}));
  60.         }
  61.  
  62.         _mock () {
  63.                 // Do this here rather than in the constructor to ensure it's scoped to the test
  64.                 this.realFetch = this.realFetch || this.global.fetch;
  65.                 this.global.fetch = this.fetchMock;
  66.                 return this;
  67.         }
  68.  
  69.         _unMock () {
  70.                 if (this.realFetch) {
  71.                         this.global.fetch = this.realFetch;
  72.                         this.realFetch = null;
  73.                 }
  74.                 this.fallbackResponse = null;
  75.                 return this;
  76.         }
  77.  
  78.         catch (response) {
  79.                 if (this.fallbackResponse) {
  80.                         console.warn(`calling fetchMock.catch() twice - are you sure you want to overwrite the previous fallback response`);
  81.                 }
  82.                 this.fallbackResponse = response || 'ok';
  83.                 return this._mock();
  84.         }
  85.  
  86.         spy () {
  87.                 this._mock();
  88.                 return this.catch(this.realFetch)
  89.         }
  90.  
  91.         fetchMock (url, opts) {
  92.  
  93.                 let response = this.router(url, opts);
  94.  
  95.                 if (!response) {
  96.                         console.warn(`unmatched call to ${url}`);
  97.                         this.push(null, [url, opts]);
  98.  
  99.                         if (this.fallbackResponse) {
  100.                                 response = this.fallbackResponse;
  101.                         } else {
  102.                                 throw new Error(`unmatched call to ${url}`)
  103.                         }
  104.                 }
  105.  
  106.                 if (typeof response === 'function') {
  107.                         response = response (url, opts);
  108.                 }
  109.  
  110.                 if (typeof response.then === 'function') {
  111.                         return response.then(response => this.mockResponse(url, response, opts))
  112.                 } else {
  113.                         return this.mockResponse(url, response, opts)
  114.                 }
  115.  
  116.         }
  117.  
  118.         router (url, opts) {
  119.                 let route;
  120.                 for (let i = 0, il = this.routes.length; i < il ; i++) {
  121.                         route = this.routes[i];
  122.                         if (route.matcher(url, opts)) {
  123.                                 this.push(route.name, [url, opts]);
  124.                                 return route.response;
  125.                         }
  126.                 }
  127.         }
  128.  
  129.         addRoute (route) {
  130.  
  131.                 if (!route) {
  132.                         throw new Error('.mock() must be passed configuration for a route')
  133.                 }
  134.  
  135.                 // Allows selective application of some of the preregistered routes
  136.                 this.routes.push(compileRoute(route, this.Request));
  137.         }
  138.  
  139.  
  140.         mockResponse (url, responseConfig, fetchOpts) {
  141.                 // It seems odd to call this in here even though it's already called within fetchMock
  142.                 // It's to handle the fact that because we want to support making it very easy to add a
  143.                 // delay to any sort of response (including responses which are defined with a function)
  144.                 // while also allowing function responses to return a Promise for a response config.
  145.                 if (typeof responseConfig === 'function') {
  146.                         responseConfig = responseConfig(url, fetchOpts);
  147.                 }
  148.  
  149.                 if (this.Response.prototype.isPrototypeOf(responseConfig)) {
  150.                         return Promise.resolve(responseConfig);
  151.                 }
  152.  
  153.                 if (responseConfig.throws) {
  154.                         return Promise.reject(responseConfig.throws);
  155.                 }
  156.  
  157.                 if (typeof responseConfig === 'number') {
  158.                         responseConfig = {
  159.                                 status: responseConfig
  160.                         };
  161.                 } else if (typeof responseConfig === 'string' || !(responseConfig.body || responseConfig.headers || responseConfig.throws || responseConfig.status)) {
  162.                         responseConfig = {
  163.                                 body: responseConfig
  164.                         };
  165.                 }
  166.  
  167.                 const opts = responseConfig.opts || {};
  168.                 opts.url = url;
  169.                 opts.sendAsJson = responseConfig.sendAsJson === undefined ? this.config.sendAsJson : responseConfig.sendAsJson;
  170.                 if (responseConfig.status && (typeof responseConfig.status !== 'number' || parseInt(responseConfig.status, 10) !== responseConfig.status || responseConfig.status < 200 || responseConfig.status > 599)) {
  171.                         throw new TypeError(`Invalid status ${responseConfig.status} passed on response object.
  172. To respond with a JSON object that has status as a property assign the object to body
  173. e.g. {"body": {"status: "registered"}}`);
  174.                 }
  175.                 opts.status = responseConfig.status || 200;
  176.                 opts.statusText = this.statusTextMap['' + opts.status];
  177.                 // The ternary operator is to cope with new Headers(undefined) throwing in Chrome
  178.                 // https://code.google.com/p/chromium/issues/detail?id=335871
  179.                 opts.headers = responseConfig.headers ? new this.Headers(responseConfig.headers) : new this.Headers();
  180.  
  181.                 let body = responseConfig.body;
  182.                 if (opts.sendAsJson && responseConfig.body != null && typeof body === 'object') { //eslint-disable-line
  183.                         body = JSON.stringify(body);
  184.                 }
  185.  
  186.                 if (this.stream) {
  187.                         let s = new this.stream.Readable();
  188.                         if (body != null) { //eslint-disable-line
  189.                                 s.push(body, 'utf-8');
  190.                         }
  191.                         s.push(null);
  192.                         body = s;
  193.                 }
  194.  
  195.                 return Promise.resolve(new this.Response(body, opts));
  196.         }
  197.  
  198.         push (name, call) {
  199.                 if (name) {
  200.                         this._calls[name] = this._calls[name] || [];
  201.                         this._calls[name].push(call);
  202.                         this._matchedCalls.push(call);
  203.                 } else {
  204.                         this._unmatchedCalls.push(call);
  205.                 }
  206.         }
  207.  
  208.         restore () {
  209.                 this._unMock();
  210.                 this.reset();
  211.                 this.routes = [];
  212.                 return this;
  213.         }
  214.  
  215.         reset () {
  216.                 this._calls = {};
  217.                 this._matchedCalls = [];
  218.                 this._unmatchedCalls = [];
  219.                 this.routes.forEach(route => route.reset && route.reset())
  220.                 return this;
  221.         }
  222.  
  223.         calls (name) {
  224.                 return name ? (this._calls[name] || []) : {
  225.                         matched: this._matchedCalls,
  226.                         unmatched: this._unmatchedCalls
  227.                 };
  228.         }
  229.  
  230.         lastCall (name) {
  231.                 const calls = name ? this.calls(name) : this.calls().matched;
  232.                 if (calls && calls.length) {
  233.                         return calls[calls.length - 1];
  234.                 } else {
  235.                         return undefined;
  236.                 }
  237.         }
  238.  
  239.         lastUrl (name) {
  240.                 const call = this.lastCall(name);
  241.                 return call && call[0];
  242.         }
  243.  
  244.         lastOptions (name) {
  245.                 const call = this.lastCall(name);
  246.                 return call && call[1];
  247.         }
  248.  
  249.         called (name) {
  250.                 if (!name) {
  251.                         return !!(this._matchedCalls.length || this._unmatchedCalls.length);
  252.                 }
  253.                 return !!(this._calls[name] && this._calls[name].length);
  254.         }
  255.  
  256.         done (name) {
  257.                 const names = name ? [name] : this.routes.map(r => r.name);
  258.                 // Ideally would use array.every, but not widely supported
  259.                 return names.map(name => {
  260.                         if (!this.called(name)) {
  261.                                 return false
  262.                         }
  263.                         // would use array.find... but again not so widely supported
  264.                         const expectedTimes = (this.routes.filter(r => r.name === name) || [{}])[0].times;
  265.                         return !expectedTimes || (expectedTimes <= this.calls(name).length)
  266.                 })
  267.                         .filter(bool => !bool).length === 0
  268.         }
  269.  
  270.         configure (opts) {
  271.                 Object.assign(this.config, opts);
  272.         }
  273. }
  274.  
  275. ['get','post','put','delete','head', 'patch']
  276.         .forEach(method => {
  277.                 FetchMock.prototype[method] = function (matcher, response, options) {
  278.                         return this.mock(matcher, response, Object.assign({}, options, {method: method.toUpperCase()}));
  279.                 }
  280.                 FetchMock.prototype[`${method}Once`] = function (matcher, response, options) {
  281.                         return this.once(matcher, response, Object.assign({}, options, {method: method.toUpperCase()}));
  282.                 }
  283.         })
  284.  
  285. module.exports = function (opts) {
  286.         return new FetchMock(opts);
  287. }