xhrio_test.js

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

goog.provide('goog.net.XhrIoTest');
goog.setTestOnly('goog.net.XhrIoTest');

goog.require('goog.Uri');
goog.require('goog.debug.EntryPointMonitor');
goog.require('goog.debug.ErrorHandler');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events');
goog.require('goog.functions');
goog.require('goog.net.EventType');
goog.require('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XmlHttp');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.net.XhrIo');
goog.require('goog.testing.recordFunction');

function MockXmlHttp() {
  /**
   * The headers for this XmlHttpRequest.
   * @type {!Object.<string>}
   */
  this.headers = {};
}

MockXmlHttp.prototype.readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;

MockXmlHttp.prototype.status = 200;

MockXmlHttp.syncSend = false;

MockXmlHttp.prototype.send = function(opt_data) {
  this.readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;

  if (MockXmlHttp.syncSend) {
    this.complete();
  }

};

MockXmlHttp.prototype.complete = function() {
  this.readyState = goog.net.XmlHttp.ReadyState.LOADING;
  this.onreadystatechange();

  this.readyState = goog.net.XmlHttp.ReadyState.LOADED;
  this.onreadystatechange();

  this.readyState = goog.net.XmlHttp.ReadyState.INTERACTIVE;
  this.onreadystatechange();

  this.readyState = goog.net.XmlHttp.ReadyState.COMPLETE;
  this.onreadystatechange();
};


MockXmlHttp.prototype.open = function(verb, uri, async) {
};

MockXmlHttp.prototype.abort = function() {};

MockXmlHttp.prototype.setRequestHeader = function(key, value) {
  this.headers[key] = value;
};

var lastMockXmlHttp;
goog.net.XmlHttp.setGlobalFactory(new goog.net.WrapperXmlHttpFactory(
    function() {
      lastMockXmlHttp = new MockXmlHttp();
      return lastMockXmlHttp;
    },
    function() {
      return {};
    }));


var propertyReplacer = new goog.testing.PropertyReplacer();
var clock;
var originalEntryPoint =
    goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_;

function setUp() {
  lastMockXmlHttp = null;
  clock = new goog.testing.MockClock(true);
}

function tearDown() {
  propertyReplacer.reset();
  clock.dispose();
  goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ = originalEntryPoint;
}


function testSyncSend() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertTrue('Should be succesful', e.target.isSuccess());
    count++;

  });

  var inSend = true;
  x.send('url');
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}

function testSyncSendFailure() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertFalse('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send('url');
  lastMockXmlHttp.status = 404;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendRelativeZeroStatus() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertEquals('Should be the same as ', e.target.isSuccess(),
        window.location.href.toLowerCase().indexOf('file:') == 0);
    count++;
  });

  var inSend = true;
  x.send('relative');
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendRelativeUriZeroStatus() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertEquals('Should be the same as ', e.target.isSuccess(),
        window.location.href.toLowerCase().indexOf('file:') == 0);
    count++;
  });

  var inSend = true;
  x.send(goog.Uri.parse('relative'));
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendHttpZeroStatusFailure() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertFalse('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send('http://foo');
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendHttpUpperZeroStatusFailure() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertFalse('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send('HTTP://foo');
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendHttpUpperUriZeroStatusFailure() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertFalse('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send(goog.Uri.parse('HTTP://foo'));
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendHttpUriZeroStatusFailure() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertFalse('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send(goog.Uri.parse('http://foo'));
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendHttpUriZeroStatusFailure() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertFalse('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send(goog.Uri.parse('HTTP://foo'));
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendHttpsZeroStatusFailure() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertFalse('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send('https://foo');
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendFileUpperZeroStatusSuccess() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertTrue('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send('FILE:///foo');
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendFileUriZeroStatusSuccess() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertTrue('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send(goog.Uri.parse('file:///foo'));
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendDummyUriZeroStatusSuccess() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertTrue('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send(goog.Uri.parse('dummy:///foo'));
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendFileUpperUriZeroStatusSuccess() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertFalse('Should not fire complete from inside send', inSend);
    assertTrue('Should not be succesful', e.target.isSuccess());
    count++;
  });

  var inSend = true;
  x.send(goog.Uri.parse('FILE:///foo'));
  lastMockXmlHttp.status = 0;
  inSend = false;

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testSendFromListener() {
  MockXmlHttp.syncSend = true;
  var count = 0;

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    count++;

    var e = assertThrows(function() {
      x.send('url2');
    });
    assertEquals('[goog.net.XhrIo] Object is active with another request=url' +
        '; newUri=url2', e.message);
  });

  x.send('url');

  clock.tick(1); // callOnce(f, 0, ...)

  assertEquals('Complete should have been called once', 1, count);
}


function testStatesDuringEvents() {
  MockXmlHttp.syncSend = true;

  var x = new goog.net.XhrIo;
  var readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;
  goog.events.listen(x, goog.net.EventType.READY_STATE_CHANGE, function(e) {
    readyState++;
    assertObjectEquals(e.target, x);
    assertEquals(x.getReadyState(), readyState);
    assertTrue(x.isActive());
  });
  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    assertObjectEquals(e.target, x);
    assertTrue(x.isActive());
  });
  goog.events.listen(x, goog.net.EventType.SUCCESS, function(e) {
    assertObjectEquals(e.target, x);
    assertTrue(x.isActive());
  });
  goog.events.listen(x, goog.net.EventType.READY, function(e) {
    assertObjectEquals(e.target, x);
    assertFalse(x.isActive());
  });

  x.send('url');

  clock.tick(1); // callOnce(f, 0, ...)
}


function testProtectEntryPointCalledOnAsyncSend() {
  MockXmlHttp.syncSend = false;

  var errorHandlerCallbackCalled = false;
  var errorHandler = new goog.debug.ErrorHandler(function() {
    errorHandlerCallbackCalled = true;
  });

  goog.net.XhrIo.protectEntryPoints(errorHandler);

  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.READY_STATE_CHANGE, function(e) {
    throw Error();
  });

  x.send('url');
  assertThrows(function() {
    lastMockXmlHttp.complete();
  });

  assertTrue('Error handler callback should be called on async send.',
      errorHandlerCallbackCalled);
}

function testXHRIsDiposedEvenIfAListenerThrowsAnExceptionOnComplete() {
  MockXmlHttp.syncSend = false;

  var x = new goog.net.XhrIo;

  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    throw Error();
  }, false, x);

  x.send('url');
  assertThrows(function() {
    lastMockXmlHttp.complete();
  });

  // The XHR should have been disposed, even though the listener threw an
  // exception.
  assertNull(x.xhr_);
}

function testDisposeInternalDoesNotAbortXhrRequestObjectWhenActiveIsFalse() {
  MockXmlHttp.syncSend = false;

  var xmlHttp = goog.net.XmlHttp;
  var abortCalled = false;
  var x = new goog.net.XhrIo;

  goog.net.XmlHttp.prototype.abort = function() { abortCalled = true; };

  goog.events.listen(x, goog.net.EventType.COMPLETE, function(e) {
    this.active_ = false;
    this.dispose();
  }, false, x);

  x.send('url');
  lastMockXmlHttp.complete();

  goog.net.XmlHttp = xmlHttp;
  assertFalse(abortCalled);
}

function testCallingAbortFromWithinAbortCallbackDoesntLoop() {
  var x = new goog.net.XhrIo;
  goog.events.listen(x, goog.net.EventType.ABORT, function(e) {
    x.abort(); // Shouldn't get a stack overflow
  });
  x.send('url');
  x.abort();
}

function testPostSetsContentTypeHeader() {
  var x = new goog.net.XhrIo;

  x.send('url', 'POST', 'content');
  var headers = lastMockXmlHttp.headers;
  assertEquals(1, goog.object.getCount(headers));
  assertEquals(
      headers[goog.net.XhrIo.CONTENT_TYPE_HEADER],
      goog.net.XhrIo.FORM_CONTENT_TYPE);
}

function testNonPostSetsContentTypeHeader() {
  var x = new goog.net.XhrIo;

  x.send('url', 'PUT', 'content');
  headers = lastMockXmlHttp.headers;
  assertEquals(1, goog.object.getCount(headers));
  assertEquals(
      headers[goog.net.XhrIo.CONTENT_TYPE_HEADER],
      goog.net.XhrIo.FORM_CONTENT_TYPE);
}

function testContentTypeIsTreatedCaseInsensitively() {
  var x = new goog.net.XhrIo;

  x.send('url', 'POST', 'content', {'content-type': 'testing'});

  assertObjectEquals(
      'Headers should not be modified since they already contain a ' +
      'content type definition',
      {'content-type': 'testing'},
      lastMockXmlHttp.headers);
}

function testIsContentTypeHeader_() {
  assertTrue(goog.net.XhrIo.isContentTypeHeader_('content-type'));
  assertTrue(goog.net.XhrIo.isContentTypeHeader_('Content-type'));
  assertTrue(goog.net.XhrIo.isContentTypeHeader_('CONTENT-TYPE'));
  assertTrue(goog.net.XhrIo.isContentTypeHeader_('Content-Type'));
  assertFalse(goog.net.XhrIo.isContentTypeHeader_('Content Type'));
}

function testPostFormDataDoesNotSetContentTypeHeader() {
  function FakeFormData() {}

  propertyReplacer.set(goog.global, 'FormData', FakeFormData);

  var x = new goog.net.XhrIo;
  x.send('url', 'POST', new FakeFormData());
  var headers = lastMockXmlHttp.headers;
  assertTrue(goog.object.isEmpty(headers));
}

function testNonPostFormDataDoesNotSetContentTypeHeader() {
  function FakeFormData() {}

  propertyReplacer.set(goog.global, 'FormData', FakeFormData);

  var x = new goog.net.XhrIo;
  x.send('url', 'PUT', new FakeFormData());
  headers = lastMockXmlHttp.headers;
  assertTrue(goog.object.isEmpty(headers));
}

function testFactoryInjection() {
  var xhr = new MockXmlHttp();
  var optionsFactoryCalled = 0;
  var xhrFactoryCalled = 0;
  var wrapperFactory = new goog.net.WrapperXmlHttpFactory(
      function() {
        xhrFactoryCalled++;
        return xhr;
      },
      function() {
        optionsFactoryCalled++;
        return {};
      });
  var xhrIo = new goog.net.XhrIo(wrapperFactory);

  xhrIo.send('url');

  assertEquals('XHR factory should have been called', 1, xhrFactoryCalled);
  assertEquals('Options factory should have been called', 1,
      optionsFactoryCalled);
}

function testGoogTestingNetXhrIoIsInSync() {
  var xhrIo = new goog.net.XhrIo();
  var testingXhrIo = new goog.testing.net.XhrIo();

  var propertyComparator = function(value, key, obj) {
    if (goog.string.endsWith(key, '_')) {
      // Ignore private properties/methods
      return true;
    } else if (typeof value == 'function' && typeof this[key] != 'function') {
      // Only type check is sufficient for functions
      fail('Mismatched property:' + key + ': gooo.net.XhrIo has:<' +
          value + '>; while goog.testing.net.XhrIo has:<' + this[key] + '>');
      return true;
    } else {
      // Ignore all other type of properties.
      return true;
    }
  };

  goog.object.every(xhrIo, propertyComparator, testingXhrIo);
}

function testEntryPointRegistry() {
  var monitor = new goog.debug.EntryPointMonitor();
  var replacement = function() {};
  monitor.wrap = goog.testing.recordFunction(
      goog.functions.constant(replacement));

  goog.debug.entryPointRegistry.monitorAll(monitor);
  assertTrue(monitor.wrap.getCallCount() >= 1);
  assertEquals(
      replacement,
      goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_);
}

function testSetWithCredentials() {
  // Test on XHR objects that don't have the withCredentials property (older
  // browsers).
  var x = new goog.net.XhrIo;
  x.setWithCredentials(true);
  x.send('url');
  assertFalse(
      'withCredentials should not be set on an XHR object if the property ' +
      'does not exist.',
      goog.object.containsKey(lastMockXmlHttp, 'withCredentials'));

  // Test on XHR objects that have the withCredentials property.
  MockXmlHttp.prototype.withCredentials = false;
  x = new goog.net.XhrIo;
  x.setWithCredentials(true);
  x.send('url');
  assertTrue(
      'withCredentials should be set on an XHR object if the property exists',
      goog.object.containsKey(lastMockXmlHttp, 'withCredentials'));

  assertTrue(
      'withCredentials value not set on XHR object',
      lastMockXmlHttp.withCredentials);

  // Reset the prototype so it does not effect other tests.
  delete MockXmlHttp.prototype.withCredentials;
}

function testGetResponse() {
  var x = new goog.net.XhrIo;

  // No XHR yet
  assertEquals(null, x.getResponse());

  // XHR with no .response and no response type, gets text.
  x.xhr_ = {};
  x.xhr_.responseText = 'text';
  assertEquals('text', x.getResponse());

  // Response type of text gets text as well.
  x.setResponseType(goog.net.XhrIo.ResponseType.TEXT);
  x.xhr_.responseText = '';
  assertEquals('', x.getResponse());

  // Response type of array buffer gets the array buffer.
  x.xhr_.mozResponseArrayBuffer = 'ab';
  x.setResponseType(goog.net.XhrIo.ResponseType.ARRAY_BUFFER);
  assertEquals('ab', x.getResponse());

  // With a response field, it is returned no matter what value it has.
  x.xhr_.response = undefined;
  assertEquals(undefined, x.getResponse());

  x.xhr_.response = null;
  assertEquals(null, x.getResponse());

  x.xhr_.response = '';
  assertEquals('', x.getResponse());

  x.xhr_.response = 'resp';
  assertEquals('resp', x.getResponse());
}

function testGetResponseHeaders() {
  var x = new goog.net.XhrIo();

  // No XHR yet
  assertEquals(0, goog.object.getCount(x.getResponseHeaders()));

  // Simulate an XHR with 2 headers.
  var headersRaw = 'test1: foo\r\ntest2: bar';

  propertyReplacer.set(x, 'getAllResponseHeaders',
                       goog.functions.constant(headersRaw));

  var headers = x.getResponseHeaders();
  assertEquals(2, goog.object.getCount(headers));
  assertEquals('foo', headers['test1']);
  assertEquals('bar', headers['test2']);
}

function testGetResponseHeadersWithColonInValue() {
  var x = new goog.net.XhrIo();

  // Simulate an XHR with a colon in the http header value.
  var headersRaw = 'test1: f:o:o';

  propertyReplacer.set(x, 'getAllResponseHeaders',
                       goog.functions.constant(headersRaw));

  var headers = x.getResponseHeaders();
  assertEquals(1, goog.object.getCount(headers));
  assertEquals('f:o:o', headers['test1']);
}

function testGetResponseHeadersMultipleValuesForOneKey() {
  var x = new goog.net.XhrIo();

  // No XHR yet
  assertEquals(0, goog.object.getCount(x.getResponseHeaders()));

  // Simulate an XHR with 2 headers.
  var headersRaw = 'test1: foo\r\ntest1: bar';

  propertyReplacer.set(x, 'getAllResponseHeaders',
                       goog.functions.constant(headersRaw));

  var headers = x.getResponseHeaders();
  assertEquals(1, goog.object.getCount(headers));
  assertEquals('foo, bar', headers['test1']);
}

function testGetResponseHeadersEmptyHeader() {
  var x = new goog.net.XhrIo();

  // No XHR yet
  assertEquals(0, goog.object.getCount(x.getResponseHeaders()));

  // Simulate an XHR with 2 headers, the last of which is empty.
  var headersRaw = 'test2: bar\r\n';

  propertyReplacer.set(x, 'getAllResponseHeaders',
                       goog.functions.constant(headersRaw));

  var headers = x.getResponseHeaders();
  assertEquals(1, goog.object.getCount(headers));
  assertEquals('bar', headers['test2']);
}