UNPKG

edx-ui-toolkit

Version:

A JavaScript toolkit for building edX user interfaces

309 lines (285 loc) 11.6 kB
/** * These utility methods are used by Jasmine tests to create a mock server or * get reference to mock requests. In either case, the cleanup (restore) is done with * an after function. * * This pattern is being used instead of the more common beforeEach/afterEach pattern * because we were seeing sporadic failures in the afterEach restore call. The cause of the * errors were that one test suite was incorrectly being linked as the parent of an unrelated * test suite (causing both suites' afterEach methods to be called). No solution for the root * cause has been found, but initializing sinon and cleaning it up on a method-by-method * basis seems to work. For more details, see STUD-1264. * * @module AjaxHelpers */ define(['sinon', 'underscore', 'URI'], function(sinon, _, URI) { 'use strict'; var XHR_READY_STATES, fakeServer, createFakeRequests, withFakeRequests, fakeRequests, currentRequest, expectRequest, expectNoRequests, expectJsonRequest, expectPostRequest, expectRequestURL, skipResetRequest, respond, respondWithJson, respondWithError, respondWithTextError, respondWithNoContent; /** * An enumeration of valid XHR ready states. */ XHR_READY_STATES = { UNSENT: 0, OPENED: 1, LOADING: 3, DONE: 4 }; /* These utility methods are used by Jasmine tests to create a mock server or * get reference to mock requests. In either case, the cleanup (restore) is done with * an after function. * * This pattern is being used instead of the more common beforeEach/afterEach pattern * because we were seeing sporadic failures in the afterEach restore call. The cause of the * errors were that one test suite was incorrectly being linked as the parent of an unrelated * test suite (causing both suites' afterEach methods to be called). No solution for the root * cause has been found, but initializing sinon and cleaning it up on a method-by-method * basis seems to work. For more details, see STUD-1264. */ /** * Get a reference to the mocked server, and respond to all requests * with the specified response. * * @param {string} response the fake response. * @returns {*} The current request. */ fakeServer = function(response) { var server = sinon.fakeServer.create(); afterEach(function() { if (server) { server.restore(); } }); server.respondWith(response); return server; }; createFakeRequests = function() { var requests = [], xhr = sinon.useFakeXMLHttpRequest(); requests.currentIndex = 0; requests.restore = function() { if (xhr && 'restore' in xhr) { xhr.restore(); } }; xhr.onCreate = function(request) { requests.push(request); }; return requests; }; /** * Keep track of all requests to a fake server, and return a reference to the Array. * This allows tests to respond to individual requests. * * @returns {Array} An array tracking the fake requests. */ fakeRequests = function() { var requests = createFakeRequests(); afterEach(function() { requests.restore(); }); return requests; }; /** * Wraps a test function so that it is invoked with an additional parameter * containing an array of fake HTTP requests. * * @param {function} test A function to be invoked with the fake requests. * @returns {function} A wrapped version of the input function. */ withFakeRequests = function(test) { return function() { var requests = createFakeRequests(), args = Array.prototype.slice.call(arguments); test.apply(null, args.concat([requests])); requests.restore(); }; }; /** * Returns the request that has not yet been responded to. If no such request * is available then the current test will fail. * * @param {object} requests an array of fired sinon requests * @returns {*} The current request. */ currentRequest = function(requests) { expect(requests.length).toBeGreaterThan(requests.currentIndex); return requests[requests.currentIndex]; }; /** * Expect that a request was made as expected. * * @param {object} requests an array of fired sinon requests * @param {string} method the expected method of the request * @param {string} url the expected url of the request * @param {string} body the expected request body */ expectRequest = function(requests, method, url, body) { var request = currentRequest(requests); expect(request.readyState).toEqual(XHR_READY_STATES.OPENED); expect(request.url).toEqual(url); expect(request.method).toEqual(method); if (typeof body === 'undefined') { // The body of the request may not be germane to the current test, // such as a call by a library, so allow it to be ignored. return; } expect(request.requestBody).toEqual(body); }; /** * Verifies that there are no unconsumed requests. * * @param {object} requests an array of fired sinon requests */ expectNoRequests = function(requests) { expect(requests.length).toEqual(requests.currentIndex); }; /** * Expect that a request with a JSON payload was made as expected. * * @param {object} requests an array of fired sinon requests * @param {string} method the expected method of the request * @param {string} url the expected url of the request * @param {object} jsonRequest the expected request body as an object */ expectJsonRequest = function(requests, method, url, jsonRequest) { var request = currentRequest(requests); expect(request.readyState).toEqual(XHR_READY_STATES.OPENED); expect(request.url).toEqual(url); expect(request.method).toEqual(method); expect(JSON.parse(request.requestBody)).toEqual(jsonRequest === undefined ? null : jsonRequest); }; /** * Expect that a JSON request be made with the given URL and parameters. * * @param {object} requests The collected requests * @param {string} expectedUrl The expected URL excluding the parameters * @param {object} expectedParameters An object representing the URL parameters */ expectRequestURL = function(requests, expectedUrl, expectedParameters) { var request = currentRequest(requests), parameters; expect(new URI(request.url).path()).toEqual(expectedUrl); parameters = new URI(request.url).query(true); delete parameters._; // Ignore the cache-busting argument expect(parameters).toEqual(expectedParameters); }; /** * Intended for use with POST requests using application/x-www-form-urlencoded. * * @param {object} requests an array of fired sinon requests * @param {string} url the expected url of the request * @param {string} body the expected body of the request */ expectPostRequest = function(requests, url, body) { var request = currentRequest(requests); expect(request.readyState).toEqual(XHR_READY_STATES.OPENED); expect(request.url).toEqual(url); expect(request.method).toEqual('POST'); expect(_.difference(request.requestBody.split('&'), body.split('&'))).toEqual([]); }; /** * Verify that the HTTP request was marked as reset, and then skip it. * * Note: this is typically used when code has explicitly canceled a request * after it has been sent. A good example is when a user chooses to cancel * a slow running search. * @param {object} requests an array of fired sinon requests */ skipResetRequest = function(requests) { var request = currentRequest(requests); expect(request.readyState).toEqual(XHR_READY_STATES.UNSENT); // Our ESLint config bans mutating params, but fixing this would require breaking AjaxHelpers API requests.currentIndex += 1; // eslint-disable-line no-param-reassign }; /** * Respond to a server request with a set of options: * * - `statusCode`: the status code to be returned (defaults to 200) * - `contentType`: the content type of the response (defaults to 'application/json') * - `body`: the body of the response (if JSON, it will be converted to a string) * * @param {object} requests an array of fired sinon requests * @param {object} options the options to provide to the response */ respond = function(requests, options) { var request = currentRequest(requests), statusCode = options.statusCode || 200, contentType = options.contentType || 'application/json', body = options.body || ''; request.respond(statusCode, {'Content-Type': contentType}, contentType === 'application/json' ? JSON.stringify(body || {}) : body ); requests.currentIndex += 1; // eslint-disable-line no-param-reassign }; /** * Respond to a request with JSON. * * @param {object} requests an array of fired sinon requests * @param {object} body an object to be serialized to the response */ respondWithJson = function(requests, body) { respond(requests, { body: body }); }; /** * Respond to a request with an error status code and a JSON * payload. * * @param {object} requests an array of fired sinon requests * @param {int} statusCode the HTTP status code of the response * (defaults to 500) * @param {object} body an object to be serialized to the response */ respondWithError = function(requests, statusCode, body) { respond(requests, { statusCode: statusCode || 500, body: body }); }; /** * Respond to a request with an error status code. * * @param {object} requests an array of fired sinon requests * @param {int} statusCode the HTTP status code of the response * (defaults to 500) * @param {object} body the response body as a string */ respondWithTextError = function(requests, statusCode, body) { respond(requests, { statusCode: statusCode || 500, contentType: 'text/plain', body: body }); }; /** * Respond to a request with an HTTP 204. * * @param {object} requests an array of fired sinon requests */ respondWithNoContent = function(requests) { respond(requests, { statusCode: 204 }); }; return { server: fakeServer, requests: fakeRequests, withFakeRequests: withFakeRequests, currentRequest: currentRequest, expectRequest: expectRequest, expectNoRequests: expectNoRequests, expectJsonRequest: expectJsonRequest, expectPostRequest: expectPostRequest, expectRequestURL: expectRequestURL, skipResetRequest: skipResetRequest, respond: respond, respondWithJson: respondWithJson, respondWithError: respondWithError, respondWithTextError: respondWithTextError, respondWithNoContent: respondWithNoContent }; });