xhr_test.js

// Copyright 2011 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.labs.net.xhrTest');
goog.setTestOnly('goog.labs.net.xhrTest');

goog.require('goog.Promise');
goog.require('goog.labs.net.xhr');
goog.require('goog.net.XmlHttp');
goog.require('goog.string');
goog.require('goog.testing.AsyncTestCase');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.jsunit');
goog.require('goog.userAgent');

function setupStubXMLHttpRequest() {

  mockClock = new goog.testing.MockClock(true);

  var stubXhr = {
    sent: false,
    aborted: false,
    status: 0,
    headers: {},
    open: function(method, url, async) {
      this.method = method;
      this.url = url;
      this.async = async;
    },
    setRequestHeader: function(key, value) {
      this.headers[key] = value;
    },
    overrideMimeType: function(mimeType) {
      this.mimeType = mimeType;
    },
    abort: function() {
      this.aborted = true;
      this.load(0);
    },
    send: function(data) {
      this.data = data;
      this.sent = true;
    },
    load: function(status) {
      this.status = status;
      this.readyState = 4;
      if (this.onreadystatechange) this.onreadystatechange();
    }
  };

  goog.net.XmlHttp = function() {
    return stubXhr;
  };
  for (var x in originalXmlHttp) {
    goog.net.XmlHttp[x] = originalXmlHttp[x];
  }

  return stubXhr;
}


var xhr = goog.labs.net.xhr;
var originalXmlHttp = goog.net.XmlHttp;
var mockClock;

var testCase = new goog.testing.AsyncTestCase(document.title);
testCase.stepTimeout = 5 * 1000;

testCase.autoDiscoverTests();

testCase.tearDown = function() {
  if (mockClock) {
    mockClock.dispose();
    mockClock = null;
  }
  goog.net.XmlHttp = originalXmlHttp;
};

G_testRunner.initialize(testCase);

// Many tests don't work on the local file system due to cross-origin
// restrictions in Chrome without --allow-file-access-from-files.
// They will run on the farm or on a Closure Test server.
var shouldRunLocally = goog.userAgent.IE || goog.userAgent.GECKO ||
    goog.string.startsWith(document.location.href, 'file://');

function testSimpleRequest() {
  if (shouldRunLocally) return;

  testCase.waitForAsync('simpleRequest');
  xhr.send('GET', 'testdata/xhr_test_text.data').then(function(xhr) {
    assertEquals('Just some data.', xhr.responseText);
    assertEquals(200, xhr.status);
    testCase.continueTesting();
  }, fail /* opt_onRejected */);
}

function testGetText() {
  if (shouldRunLocally) return;

  testCase.waitForAsync('getText');
  xhr.get('testdata/xhr_test_text.data').then(function(responseText) {
    assertEquals('Just some data.', responseText);
    testCase.continueTesting();
  }, fail /* opt_onRejected */);
}

function testGetTextWithJson() {
  if (shouldRunLocally) return;

  testCase.waitForAsync('getTextWithJson');
  xhr.get('testdata/xhr_test_json.data').then(function(responseText) {
    assertEquals('while(1);\n{"stat":"ok","count":12345}\n', responseText);
    testCase.continueTesting();
  }, fail /* opt_onRejected */);
}

function testPostText() {
  if (shouldRunLocally) return;

  testCase.waitForAsync('postText');
  xhr.post('testdata/xhr_test_text.data', 'post-data').then(
      function(responseText) {
        // No good way to test post-data gets transported.
        assertEquals('Just some data.', responseText);
        testCase.continueTesting();
      }, fail /* opt_onRejected */);
}

function testGetJson() {
  if (shouldRunLocally) return;

  testCase.waitForAsync('getJson');
  xhr.getJson('testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'}).then(
      function(responseObj) {
        assertEquals('ok', responseObj['stat']);
        assertEquals(12345, responseObj['count']);
        testCase.continueTesting();
      }, fail /* opt_onRejected */);
}

function testSerialRequests() {
  if (shouldRunLocally) return;

  xhr.get('testdata/xhr_test_text.data').
      then(function(response) {
        return xhr.getJson(
            'testdata/xhr_test_json.data', {xssiPrefix: 'while(1);\n'});
      }).then(function(responseObj) {
        // Data that comes through to callbacks should be from the 2nd request.
        assertEquals('ok', responseObj['stat']);
        assertEquals(12345, responseObj['count']);
        testCase.continueTesting();
      }, fail /* opt_onRejected */);
}

function testBadUrlDetectedAsError() {
  if (shouldRunLocally) return;

  testCase.waitForAsync('badUrl');
  xhr.getJson('unknown-file.dat').then(
      fail /* opt_onFulfilled */,
      function(err) {
        assertTrue(
            'Error should be an HTTP error', err instanceof xhr.HttpError);
        assertEquals(404, err.status);
        assertNotNull(err.xhr);
        testCase.continueTesting();
      });
}

function testBadOriginTriggersOnErrorHandler() {
  testCase.waitForAsync('badOrigin');
  xhr.get('http://www.google.com').then(
      fail /* opt_onFulfilled */,
      function(err) {
        // In IE this will be a goog.labs.net.xhr.Error since it is thrown
        //  when calling xhr.open(), other browsers will raise an HttpError.
        assertTrue('Error should be an xhr error', err instanceof xhr.Error);
        assertNotNull(err.xhr);
        testCase.continueTesting();
      });
}

function testAbortRequest() {
  if (shouldRunLocally) return;

  testCase.waitForAsync('abortRequest');
  var promise = xhr.send('GET', 'test-url', null).then(
      fail /* opt_onFulfilled */,
      function(error, result) {
        assertTrue(error instanceof goog.Promise.CancellationError);
        testCase.continueTesting();
      });
  promise.cancel();
}

//============================================================================
// The following tests are synchronous and use a stubbed out XMLHttpRequest.
//============================================================================

function testSendNoOptions() {
  var stubXhr = setupStubXMLHttpRequest();
  var called = false;
  xhr.send('GET', 'test-url', null).then(function(xhr) {
    called = true;
    assertEquals('Objects should be equal', xhr, stubXhr);
  }, fail /* opt_onRejected */);

  assertTrue('XHR should have been sent', stubXhr.sent);
  assertFalse('Callback should not yet have been called', called);

  stubXhr.load(200);
  mockClock.tick();

  assertTrue('Callback should have been called', called);
  assertEquals('GET', stubXhr.method);
  assertEquals('test-url', stubXhr.url);
}

function testSendPostSetsDefaultHeader() {
  var stubXhr = setupStubXMLHttpRequest();
  xhr.send('POST', 'test-url', null).
      then(undefined /* opt_onResolved */, fail /* opt_onRejected */);

  stubXhr.load(200);
  mockClock.tick();

  assertEquals('POST', stubXhr.method);
  assertEquals('test-url', stubXhr.url);
  assertEquals('application/x-www-form-urlencoded;charset=utf-8',
      stubXhr.headers['Content-Type']);
}

function testSendPostHeaders() {
  var stubXhr = setupStubXMLHttpRequest();
  xhr.send('POST', 'test-url', null, {
    headers: {'Content-Type': 'text/plain', 'X-Made-Up': 'FooBar'}
  }).then(undefined /* opt_onResolved */, fail /* opt_onRejected */);

  stubXhr.load(200);
  mockClock.tick();

  assertEquals('POST', stubXhr.method);
  assertEquals('test-url', stubXhr.url);
  assertEquals('text/plain', stubXhr.headers['Content-Type']);
  assertEquals('FooBar', stubXhr.headers['X-Made-Up']);
}

function testSendWithCredentials() {
  var stubXhr = setupStubXMLHttpRequest();
  xhr.send('POST', 'test-url', null, {withCredentials: true}).
      then(undefined /* opt_onResolved */, fail /* opt_onRejected */);

  stubXhr.load(200);
  mockClock.tick();

  assertTrue('XHR should have been sent', stubXhr.sent);
  assertTrue(stubXhr.withCredentials);
}

function testSendWithMimeType() {
  var stubXhr = setupStubXMLHttpRequest();
  xhr.send('POST', 'test-url', null, {mimeType: 'text/plain'}).
      then(undefined /* opt_onResolved */, fail /* opt_onRejected */);

  stubXhr.load(200);
  mockClock.tick();

  assertTrue('XHR should have been sent', stubXhr.sent);
  assertEquals('text/plain', stubXhr.mimeType);
}

function testSendWithHttpError() {
  var stubXhr = setupStubXMLHttpRequest();
  var err;
  xhr.send('POST', 'test-url', null).then(
      fail /* opt_onResolved */,
      function(error) { err = error; } /* opt_onRejected */);

  stubXhr.load(500);
  mockClock.tick();

  assertTrue('XHR should have been sent', stubXhr.sent);
  assertTrue(err instanceof xhr.HttpError);
  assertEquals(500, err.status);
}

function testSendWithTimeoutNotHit() {
  // TODO(user): This test fails in safari if it is run as part of a batch
  // but passes when run on its own.  Something strange is going on to do
  // with the references to window.clearTimeout inside onreadystatechange and
  // the mockclock overrides.
  if (goog.userAgent.SAFARI) return;

  var stubXhr = setupStubXMLHttpRequest();
  var err;
  xhr.send('POST', 'test-url', null, {timeoutMs: 1500}).
      then(undefined /* opt_onResolved */, fail /* opt_onRejected */);
  assertTrue(mockClock.getTimeoutsMade() > 0);
  mockClock.tick(1400);
  stubXhr.load(200);
  mockClock.tick(200);
  assertTrue('XHR should have been sent', stubXhr.sent);
  assertFalse('XHR should not have been aborted', stubXhr.aborted);
}

function testSendWithTimeoutHit() {
  var stubXhr = setupStubXMLHttpRequest();
  var err;
  xhr.send('POST', 'test-url', null, {timeoutMs: 50}).then(
      fail /* opt_onResolved */,
      function(error) { err = error; } /* opt_onRejected */);
  assertTrue(mockClock.getTimeoutsMade() > 0);
  mockClock.tick(50);
  assertTrue('XHR should have been sent', stubXhr.sent);
  assertTrue('XHR should have been aborted', stubXhr.aborted);
  assertTrue(err instanceof xhr.TimeoutError);
}

function testCancelRequest() {
  var stubXhr = setupStubXMLHttpRequest();
  var err;
  var promise = xhr.send('GET', 'test-url', null, {timeoutMs: 50}).then(
      fail /* opt_onResolved */,
      function(error) {
        err = error;
        if (error instanceof goog.Promise.CancellationError) {
          stubXhr.abort();
        }
      });
  promise.cancel();
  stubXhr.load(0);  // Call load anyway, shoudn't make a difference.
  mockClock.tick(100);  // Timeout should never be called.

  assertTrue('XHR should have been sent', stubXhr.sent);
  assertTrue('XHR should have been aborted', stubXhr.aborted);
  assertTrue(err instanceof goog.Promise.CancellationError);
}

function testGetJson() {
  var stubXhr = setupStubXMLHttpRequest();
  var responseData;
  xhr.getJson('test-url').then(function(responseObj) {
    responseData = responseObj;
  }, fail /* opt_onRejected */);

  stubXhr.responseText = '{"a": 1, "b": 2}';
  stubXhr.load(200);
  mockClock.tick();

  assertObjectEquals({a: 1, b: 2}, responseData);
}

function testGetJsonWithXssiPrefix() {
  var stubXhr = setupStubXMLHttpRequest();
  var responseData;
  xhr.getJson('test-url', {xssiPrefix: 'while(1);\n'}).then(
      function(responseObj) { responseData = responseObj; },
      fail /* opt_onRejected */);

  stubXhr.responseText = 'while(1);\n{"a": 1, "b": 2}';
  stubXhr.load(200);
  mockClock.tick();

  assertObjectEquals({a: 1, b: 2}, responseData);
}

function testSendWithClientException() {
  var stubXhr = setupStubXMLHttpRequest();
  stubXhr.send = function(data) {
    throw new Error('CORS XHR with file:// schemas not allowed.');
  };
  var err;
  xhr.send('POST', 'file://test-url', null).then(
      fail /* opt_onResolved */,
      function(error) { err = error; } /* opt_onRejected */);

  stubXhr.load(0);
  mockClock.tick();

  assertFalse('XHR should not have been sent', stubXhr.sent);
  assertTrue(err instanceof Error);
  assertTrue(
      /CORS XHR with file:\/\/ schemas not allowed./.test(err.message));
}