chromium/third_party/blink/web_tests/http/tests/xmlhttprequest/timeout/xmlhttprequest-timeout.js

/* Test imported from Alex Vincent's XHR2 timeout tests, written for Mozilla.
   https://hg.mozilla.org/mozilla-central/file/tip/content/base/test/
   Released into the public domain, according to
   https://bugzilla.mozilla.org/show_bug.cgi?id=525816#c86
*/

/* Notes:
   - All times are expressed in milliseconds in this test suite.
   - Test harness code is at the end of this file.
   - We generate only one request at a time, to avoid overloading the HTTP
   request handlers.
 */

var TIME_NORMAL_LOAD = 1000;
var TIME_LATE_TIMEOUT = 800;
var TIME_XHR_LOAD = 600;
var TIME_REGULAR_TIMEOUT = 400;
var TIME_SYNC_TIMEOUT = 200;
var TIME_DELAY = 200;

/*
 * This should point to a resource that responds after a delay of TIME_XHR_LOAD milliseconds.
 */
var STALLED_REQUEST_URL = "/resources/load-and-stall.php?name=../../../http/tests/xmlhttprequest/timeout/xmlhttprequest-timeout.js&stallFor=" + TIME_XHR_LOAD/1000 + "&stallAt=0&mimeType=text/plain";

var inWorker = false;
try {
  inWorker = !(self instanceof Window);
} catch (e) {
  inWorker = true;
}

function message(obj) {
  if (inWorker)
    self.postMessage(obj);
  else
    self.postMessage(obj, "*");
}

function is(got, expected, msg) {
  var obj = {};
  obj.type = "is";
  obj.got = got;
  obj.expected = expected;
  obj.msg = msg;

  message(obj);
}

function ok(bool, msg) {
  var obj = {};
  obj.type = "ok";
  obj.bool = bool;
  obj.msg = msg;

  message(obj);
}

/**
 * Generate and track results from a XMLHttpRequest with regards to timeouts.
 *
 * @param {String} id         The test description.
 * @param {Number} timeLimit  The initial setting for the request timeout.
 * @param {Number} resetAfter (Optional) The time after sending the request, to
 *                            reset the timeout.
 * @param {Number} resetTo    (Optional) The delay to reset the timeout to.
 *
 * @note The actual testing takes place in handleEvent(event).
 * The requests are generated in startXHR().
 *
 * @note If resetAfter and resetTo are omitted, only the initial timeout setting
 * applies.
 *
 * @constructor
 * @implements DOMEventListener
 */
function RequestTracker(async, id, timeLimit /*[, resetAfter, resetTo]*/) {
  this.async = async;
  this.id = id;
  this.timeLimit = timeLimit;

  if (arguments.length > 3) {
    this.mustReset  = true;
    this.resetAfter = arguments[3];
    this.resetTo    = arguments[4];
  }

  this.hasFired = false;
}
RequestTracker.prototype = {
  /**
   * Start the XMLHttpRequest!
   */
  startXHR: function() {
    var req = new XMLHttpRequest();
    this.request = req;
    req.open("GET", STALLED_REQUEST_URL, this.async);
    var me = this;
    function handleEvent(e) { return me.handleEvent(e); };
    req.onerror = handleEvent;
    req.onload = handleEvent;
    req.onabort = handleEvent;
    req.ontimeout = handleEvent;

    req.timeout = this.timeLimit;
    
    if (this.mustReset) {
      var resetTo = this.resetTo;
      self.setTimeout(function() {
        req.timeout = resetTo;
      }, this.resetAfter);
    }

    try {
      req.send(null);
    }
    catch (e) {
      // Synchronous case in workers.
      ok(!this.async && this.timeLimit < TIME_XHR_LOAD && e.name == "TimeoutError", "Unexpected error: " + e);
      TestCounter.testComplete();
    }
  },

  /**
   * Get a message describing this test.
   *
   * @returns {String} The test description.
   */
  getMessage: function() {
    var rv = this.id + ", ";
    if (this.mustReset) {
      rv += "original timeout at " + this.timeLimit + ", ";
      rv += "reset at " + this.resetAfter + " to " + this.resetTo;
    }
    else {
      rv += "timeout scheduled at " + this.timeLimit;
    }
    return rv;
  },

  /**
   * Check the event received, and if it's the right (and only) one we get.
   *
   * @param {DOMProgressEvent} evt An event of type "load" or "timeout".
   */
  handleEvent: function(evt) {
    if (this.hasFired) {
      ok(false, "Only one event should fire: " + this.getMessage());
      return;
    }
    this.hasFired = true;

    var type = evt.type, expectedType;
    // The XHR responds after TIME_XHR_LOAD milliseconds with a load event.
    var timeLimit = this.mustReset && (this.resetAfter < Math.min(TIME_XHR_LOAD, this.timeLimit)) ?
                    this.resetTo :
                    this.timeLimit;
    if ((timeLimit == 0) || (timeLimit >= TIME_XHR_LOAD)) {
      expectedType = "load";
    }
    else {
      expectedType = "timeout";
    }
    is(type, expectedType, this.getMessage());
    TestCounter.testComplete();
  }
};

/**
 * Generate and track XMLHttpRequests which will timeout or have abort() called
 * on.
 *
 * @param shouldAbort {Boolean} True if we should call send() and then abort()
 *                              at all.
 * @param abortDelay  {Number}  The time in ms to wait before calling abort().
 */
function AbortedRequest(shouldAbort, abortDelay) {
  this.shouldAbort = shouldAbort;
  this.abortDelay  = abortDelay;
  this.hasFired    = false;
}
AbortedRequest.prototype = {
  /**
   * Start the XMLHttpRequest!
   */
  startXHR: function() {
    var req = new XMLHttpRequest();
    this.request = req;
    req.open("GET", STALLED_REQUEST_URL);
    var me = this;
    function handleEvent(e) { return me.handleEvent(e); };
    req.onerror = handleEvent;
    req.onload = handleEvent;
    req.onabort = handleEvent;
    req.ontimeout = handleEvent;

    req.timeout = TIME_REGULAR_TIMEOUT;

    function abortReq() {
      if (me.abortDelay > TIME_REGULAR_TIMEOUT) {
        is(me.request.readyState, XMLHttpRequest.DONE, "XHR must be in DONE state after timeout");
        me.ensureTimeoutEventFired();
        // req is in DONE state, so, "abort" event won't fire.
        req.abort();
      } else {
        me.ensureNoEventsFired();
        // This fires "abort" event.
        req.abort();
      }

      is(me.request.readyState, XMLHttpRequest.UNSENT, "XHR must be in UNSENT state after abort()");

      TestCounter.testComplete();
    }

    if (!this.shouldAbort) {
      self.setTimeout(function() {
        try {
          // send() has not been called. No event should be observed.
          me.ensureNoEventsFired();
        }
        catch (e) {
          ok(false, "Unexpected error: " + e);
        }

        TestCounter.testComplete();
      }, TIME_NORMAL_LOAD);
    }
    else {
      // Abort events can only be triggered on sent requests.
      req.send();
      if (this.abortDelay == -1) {
        is(req.readyState, XMLHttpRequest.OPENED, "XHR must be in OPENED state");
        // This should fire "abort" event. handleEvent checks that.
        req.abort();

        TestCounter.testComplete();
      }
      else {
        // Check state of req and call abort() on it after the specified delay.
        self.setTimeout(abortReq, this.abortDelay);
      }
    }
  },

  /**
   * Ensure that no events fired at all, especially not our timeout event.
   */
  ensureNoEventsFired: function() {
    ok(!this.hasFired, "No events should fire for an unsent, unaborted request");
  },

  /**
   * Ensure that an event fired, our timeout event.
   */
  ensureTimeoutEventFired: function() {
    ok(this.hasFired && this.eventFired == "timeout", "A timeout event should have fired");
  },

  /**
   * Get a message describing this test.
   *
   * @returns {String} The test description.
   */
  getMessage: function() {
    return "time to abort is " + this.abortDelay + ", timeout set at " + TIME_REGULAR_TIMEOUT;
  },

  /**
   * Check the event received, and if it's the right (and only) one we get.
   *
   * @param {DOMProgressEvent} evt An event of type "load" or "timeout".
   */
  handleEvent: function(evt) {
    if (this.hasFired) {
      ok(false, "Only one event should fire: " + this.getMessage());
      return;
    }

    if (!this.shouldAbort) {
      // We don't call send() and abort(). No event should fire.
      ok(false, "No event should fire: " + this.getMessage());
      return;
    }

    var expectedEvent;
    if (this.abortDelay >= TIME_REGULAR_TIMEOUT) {
      // Timeout happens earlier than abort(). abort() will be noop since the
      // XHR should be already in DONE state at that point.
      expectedEvent = "timeout";
    } else {
      // abort() will be called earlier than timeout. "abort" event should
      //  fire.
      expectedEvent = "abort";
    }

    this.hasFired = true;
    this.eventFired = evt.type;
    is(evt.type, expectedEvent, this.getMessage());
  }
};

var SyncRequestSettingTimeoutAfterOpen = {
  startXHR: function() {
    var pass = false;
    var req = new XMLHttpRequest();
    req.open("GET", STALLED_REQUEST_URL, false);
    try {
      req.timeout = TIME_SYNC_TIMEOUT;
    }
    catch (e) {
      pass = true;
    }
    ok(pass, "Synchronous XHR must not allow a timeout to be set");
    TestCounter.testComplete();
  }
};

var SyncRequestSettingTimeoutBeforeOpen = {
  startXHR: function() {
    var pass = false;
    var req = new XMLHttpRequest();
    req.timeout = TIME_SYNC_TIMEOUT;
    try {
      req.open("GET", STALLED_REQUEST_URL, false);
    }
    catch (e) {
      pass = true;
    }
    ok(pass, "Synchronous XHR must not allow a timeout to be set");
    TestCounter.testComplete();
  }
};

var TestRequestGroups = {
  "simple" : [
    new RequestTracker(true, "no time out scheduled, load fires normally", 0),
    new RequestTracker(true, "load fires normally", TIME_NORMAL_LOAD),
    new RequestTracker(true, "timeout hit before load", TIME_REGULAR_TIMEOUT)
  ],

  "twice" : [
     new RequestTracker(true, "load fires normally with no timeout set, twice", 0, TIME_REGULAR_TIMEOUT, 0),
     new RequestTracker(true, "load fires normally with same timeout set twice", TIME_NORMAL_LOAD, TIME_REGULAR_TIMEOUT, TIME_NORMAL_LOAD),
     new RequestTracker(true, "timeout fires normally with same timeout set twice", TIME_REGULAR_TIMEOUT, TIME_DELAY, TIME_REGULAR_TIMEOUT)
  ],

  // FIXME: http://webkit.org/b/98156 - Late updates are not supported yet, these tests are not run.
  "overrides" : [
    new RequestTracker(true, "timeout disabled after initially set", TIME_NORMAL_LOAD, TIME_REGULAR_TIMEOUT, 0),
    new RequestTracker(true, "timeout overrides load after a delay", TIME_NORMAL_LOAD, TIME_DELAY, TIME_REGULAR_TIMEOUT),
    new RequestTracker(true, "timeout enabled after initially disabled", 0, TIME_REGULAR_TIMEOUT, TIME_NORMAL_LOAD)
  ],

  "overridesexpires" : [
    new RequestTracker(true, "timeout set to expiring value after load fires", TIME_NORMAL_LOAD, TIME_LATE_TIMEOUT, TIME_DELAY),
    // FIXME: http://webkit.org/b/98156 - Late updates are not supported yet, this test is not run.
    // new RequestTracker(true, "timeout set to expired value before load fires", TIME_NORMAL_LOAD, TIME_REGULAR_TIMEOUT, TIME_DELAY),
    new RequestTracker(true, "timeout set to non-expiring value after timeout fires", TIME_DELAY, TIME_REGULAR_TIMEOUT, TIME_NORMAL_LOAD)
  ],

  "aborted" : [
    new AbortedRequest(false),
    new AbortedRequest(true, -1),
    new AbortedRequest(true, TIME_NORMAL_LOAD)
  ],

  "abortedonmain" : [
    new AbortedRequest(true, 0),
    new AbortedRequest(true, TIME_DELAY)
  ],

  "synconmain" : [
    SyncRequestSettingTimeoutAfterOpen,
    SyncRequestSettingTimeoutBeforeOpen
  ],

  "synconworker" : [
    new RequestTracker(false, "no time out scheduled, load fires normally", 0),
    new RequestTracker(false, "load fires normally", TIME_NORMAL_LOAD),
    new RequestTracker(false, "timeout hit before load", TIME_REGULAR_TIMEOUT)
  ]
};

var TestRequests = [];

// This code controls moving from one test to another.
var TestCounter = {
  testComplete: function() {
    // Allow for the possibility there are other events coming.
    self.setTimeout(function() {
      TestCounter.next();
    }, TIME_NORMAL_LOAD);
  },

  next: function() {
    var test = TestRequests.shift();

    if (test) {
      test.startXHR();
    }
    else {
      message("done");
    }
  }
};

self.addEventListener("message", function (event) {
  if (event.data.type == "start") {
    TestRequests = TestRequestGroups[event.data.group];
    TestCounter.next();
  }
});