Source: testing/MockXhr.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/*globals sinon*/
/**
 * This can be extended for creating widgets that provide Mock XHR responses. It is provided for use
 * in testing and will replace the standard browser XMLHttpRequest rendering all standard XHR requests
 * impossible when included on a page.
 * 
 * @module alfresco/testing/MockXhr
 * @author Dave Draper
 * @author Martin Doyle
 * @since 1.0.50
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin", 
        "dojo/text!./templates/MockXhr.html", 
        "alfresco/core/Core", 
        "dojo/_base/lang", 
        "dojo/_base/array",
        "dojo/dom-class", 
        "dojo/dom-construct", 
        "dojo/aspect", 
        "dojo/Deferred"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, AlfCore, lang, array, domClass, domConstruct, aspect, Deferred) {

   return declare([_WidgetBase, _TemplatedMixin, AlfCore], {

      /**
       * An array of the CSS files to use with this widget.
       *
       * @instance
       * @type {object[]}
       * @default [{cssFile:"./css/MockXhr.css"}]
       */
      cssRequirements: [{cssFile: "./css/MockXhr.css"}],

      /**
       * The HTML template to use for the widget.
       * @instance
       * @type {string}
       */
      templateString: template,

      /**
       * An amount of time (in milliseconds) to respond after. This is useful for giving the test a chance to
       * cancel operations. If no value is given then it will respond immediately.
       *
       * @instance
       * @type {number}
       * @default
       */
      respondAfter: null,

      /**
       * Sets up the Sinon fake server.
       *
       * @instance
       */
      constructor: function alfresco_testing_MockXhr__constructor(args) {
         lang.mixin(this, args);
         this.loadBinaryData();

         // Set-up a fake server to handle all the responses...
         var server = this.server = sinon.fakeServer.create();
         if (this.respondAfter)
         {
            this.server.autoRespondAfter = this.respondAfter;
         }
         server.autoRespond = true;
         server.xhr.useFilters = true;

         // Adds a filter to allow dynamic dependency requests to be processed normally...
         server.xhr.addFilter(function(method, url) {
           return !!url.match(/surf\/dojo\/xhr\/dependencies/) || !!url.match(/service\/sanitize\/data/);
         });
         this.setupServer();

         // Capture each request and log it...
         this.requests = [];
         aspect.before(this.server, "handleRequest", lang.hitch(this, this.updateLog));
      },

      /**
       * Build the HTML for displaying a request/response body on the page
       *
       * @instance
       * @param {object} body The body object (request or response)
       * @returns {string} The HTML
       */
      buildBodyHTML: function alfresco_testing_MockXhr__buildBodyHTML(body) {
         var bodyHTML = body;
         try {
            bodyHTML = JSON.parse(bodyHTML);
            bodyHTML = JSON.stringify(bodyHTML, null, 2);
         } catch (e) {
            // Ignore
         }
         return bodyHTML;
      },

      /**
       * Build the HTML for displaying headers on the page
       *
       * @instance
       * @param {object} headers The headers object (request or response)
       * @returns {string} The HTML
       */
      buildHeadersHTML: function alfresco_testing_MockXhr__buildHeadersHTML(headers) {
         var headersHtmlBuffer = [],
            headerName,
            headerValue;
         for (headerName in headers) {
            if (headers.hasOwnProperty(headerName)) {
               headerValue = headers[headerName];
               headersHtmlBuffer.push("<span class='nowrap'><strong>" + headerName + ":</strong> " + headerValue + "</span>");
            }
         }
         return (headersHtmlBuffer.length && headersHtmlBuffer.join("<br />")) || "N/A";
      },

      /**
       * This is an extension point function intended to be overridden by extending mock xhr services.
       * The extension should load binary data before the XMLHttpRequest object is overridden by Sinon
       * and the data loading call backs should call the [waitForServer]{@link module:aikauTesting/MockXhr#waitForServer}
       * function which will in turn call [setupServerWithBinaryData]{@link module:aikauTesting/MockXhr#setupServerWithBinaryData}
       * when the fake Sinon server is ready for configuring.
       *
       * @instance
       */
      loadBinaryData: function alfresco_testing_MockXhr__loadBinaryData() {
         // Extension point - no action required.
      },

      /**
       * This should be called from [loadBinaryData]{@link module:aikauTesting/MockXhr#loadBinaryData} once
       * binary data is loaded. It will call [setupServerWithBinaryData]{@link module:aikauTesting/MockXhr#setupServerWithBinaryData}
       * once the fake Sinon server is ready to be configured to return the binary data.
       *
       * @instance
       */
      waitForServer: function alfresco_testing_mockservices_MockXhr__waitForServer() {
         var _this = this;
         setTimeout(function() {
            if (!_this.server) {
               _this.alfLog("log", "Waiting for fake server...");
               _this.waitForServer();
            } else {
               _this.setupServerWithBinaryData();
            }
         }, 3000);
      },

      /**
       * This is an extension point function intended to be overridden by extending mock xhr services. It
       * is called from [waitForServer]{@link module:aikauTesting/MockXhr#waitForServer} and indicates that
       * both the binary data and the fake Sinon server are ready to use.
       *
       * @instance
       */
      setupServerWithBinaryData: function alfresco_testing_mockservices_MockXhr__setupServerWithBinaryData() {
         // Extension point - no action required.
      },

      /**
       * This is an extension point function intended to be overridden by extending mock xhr services. It should be
       * overridden to set up the fake server with all the responses it should provide.
       *
       * @instance
       */
      setupServer: function alfresco_testing_MockXhr__setupServer() {
         // Extension point - no action required.
         this.alfPublish("ALF_MOCK_XHR_SERVICE_READY", {});
      },

      /**
       * Adds the details of each XHR request to the log so that it can be queried by a unit test to
       * check that services are making appropriate requests for data.
       *
       * @instance
       * @param {object} xhrRequest The XHR request that was made
       */
      updateLog: function alfresco_testing_MockXhr__updateLog(xhrRequest) {

         // We need to know when the response has been completed
         var deferred = new Deferred(),
            stateComplete = 4;
         xhrRequest.addEventListener("readystatechange", function() {
            if (xhrRequest.readyState === stateComplete) {
               deferred.resolve();
            }
         });

         // Add a new row to the log
         var rowNode = domConstruct.create("tr", {
            className: "mx-row",
            "data-aikau-xhr-method": xhrRequest.method || "",
            "data-aikau-xhr-url": xhrRequest.url || "",
            "data-aikau-xhr-request-headers": (xhrRequest.requestHeaders && JSON.stringify(xhrRequest.requestHeaders)) || "",
            "data-aikau-xhr-request-body": xhrRequest.requestBody || ""
         }, this.logNode, "first");
         domConstruct.create("td", {
            className: "mx-method",
            innerHTML: xhrRequest.method,
            title: xhrRequest.method
         }, rowNode);
         domConstruct.create("td", {
            className: "mx-url",
            innerHTML: xhrRequest.url,
            title: xhrRequest.url
         }, rowNode);
         domConstruct.create("td", {
            className: "mx-request-headers",
            innerHTML: this.buildHeadersHTML(xhrRequest.requestHeaders),
            title: this.buildHeadersHTML(xhrRequest.requestHeaders)
         }, rowNode);
         domConstruct.create("td", {
            className: "mx-request-body",
            innerHTML: this.buildBodyHTML(xhrRequest.requestBody) || " ",
            title: this.buildBodyHTML(xhrRequest.requestBody) || " "
         }, rowNode);
         var responseNode = domConstruct.create("td", {
            className: "mx-response",
            innerHTML: "Waiting..."
         }, rowNode);

         // Once we've had a response, add it to the log
         deferred.promise.then(lang.hitch(this, function() {

            // Construct headers HTML and tidy response text
            var headersHtml = this.buildHeadersHTML(xhrRequest.responseHeaders),
               responseHeaders = (xhrRequest.responseHeaders && JSON.stringify(xhrRequest.responseHeaders)) || "";
            var responseBody = (xhrRequest.responseText && lang.trim(xhrRequest.responseText)) || "N/A";

            // Build response info
            var responseHtml = xhrRequest.status + " (" + xhrRequest.statusText + ")<br />";
            responseHtml += "<em>Hover for more info ...</em>";
            responseHtml += "<dl class='mx-response__info'>";
            responseHtml += "<dt>Headers</dt>";
            responseHtml += "<dd>" + headersHtml + "</dd>";
            responseHtml += "<dt>Reponse body</dt>";
            responseHtml += "<dd>" + this.buildBodyHTML(responseBody) + "</dd>";
            responseHtml += "</dl>";

            // Add a new row to the log and update the data attribute
            responseNode.innerHTML = responseHtml;
            rowNode.setAttribute("data-aikau-xhr-response-headers", responseHeaders);
            rowNode.setAttribute("data-aikau-xhr-response-body", responseBody);
         }));
      },

      /**
       * Clear the log node
       *
       * @instance
       */
      _clearLog: function alfresco_testing_MockXhr___clearLog() {
         domConstruct.empty(this.logNode);
      },

      /**
       * This top-level click handler is to prevent click events on the log bubbling back up to the document.
       *
       * @instance
       * @param {Event} evt Dojo-normalised event object
       * @since 1.0.61
       */
      _onWidgetClick: function alfresco_logging_DebugLog___onWidgetClick(evt) {
         evt.stopPropagation();
      },

      /**
       * Toggle the body visibility for the log
       *
       * @instance
       */
      _toggleBody: function alfresco_testing_MockXhr___toggleBody() {
         domClass.toggle(this.domNode, "alfresco-testing-MockXhr--hide-body");
      }
   });
});