mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-07 22:37:28 +08:00
This change brings Angular's JSONP behaviour closer in line with jQuery's. The feature has
already landed in the 1.3 branch as 6680b7b, however this alternative version is intended
to implement the feature in an IE8-compatible fashion.
Closes #7031
561 lines
16 KiB
JavaScript
561 lines
16 KiB
JavaScript
describe('$httpBackend', function() {
|
|
|
|
var $backend, $browser, callbacks,
|
|
xhr, fakeDocument, callback,
|
|
fakeTimeoutId = 0;
|
|
|
|
// TODO(vojta): should be replaced by $defer mock
|
|
function fakeTimeout(fn, delay) {
|
|
fakeTimeout.fns.push(fn);
|
|
fakeTimeout.delays.push(delay);
|
|
fakeTimeout.ids.push(++fakeTimeoutId);
|
|
return fakeTimeoutId;
|
|
}
|
|
|
|
fakeTimeout.fns = [];
|
|
fakeTimeout.delays = [];
|
|
fakeTimeout.ids = [];
|
|
fakeTimeout.flush = function() {
|
|
var len = fakeTimeout.fns.length;
|
|
fakeTimeout.delays = [];
|
|
fakeTimeout.ids = [];
|
|
while (len--) fakeTimeout.fns.shift()();
|
|
};
|
|
fakeTimeout.cancel = function(id) {
|
|
var i = indexOf(fakeTimeout.ids, id);
|
|
if (i >= 0) {
|
|
fakeTimeout.fns.splice(i, 1);
|
|
fakeTimeout.delays.splice(i, 1);
|
|
fakeTimeout.ids.splice(i, 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
|
|
beforeEach(inject(function($injector) {
|
|
callbacks = {counter: 0};
|
|
$browser = $injector.get('$browser');
|
|
fakeDocument = {
|
|
$$scripts: [],
|
|
createElement: jasmine.createSpy('createElement').andCallFake(function() {
|
|
// msie8 depends on modifying readyState for testing. This property is readonly,
|
|
// so it requires a fake object. For other browsers, we do need to make use of
|
|
// event listener registration/deregistration, so these stubs are needed.
|
|
if (msie <= 8) return {
|
|
attachEvent: noop,
|
|
detachEvent: noop,
|
|
addEventListener: noop,
|
|
removeEventListener: noop
|
|
};
|
|
// Return a proper script element...
|
|
return document.createElement(arguments[0]);
|
|
}),
|
|
body: {
|
|
appendChild: jasmine.createSpy('body.appendChild').andCallFake(function(script) {
|
|
fakeDocument.$$scripts.push(script);
|
|
}),
|
|
removeChild: jasmine.createSpy('body.removeChild').andCallFake(function(script) {
|
|
var index = indexOf(fakeDocument.$$scripts, script);
|
|
if (index != -1) {
|
|
fakeDocument.$$scripts.splice(index, 1);
|
|
}
|
|
})
|
|
}
|
|
};
|
|
$backend = createHttpBackend($browser, createMockXhr, fakeTimeout, callbacks, fakeDocument);
|
|
callback = jasmine.createSpy('done');
|
|
}));
|
|
|
|
|
|
it('should do basics - open async xhr and send data', function() {
|
|
$backend('GET', '/some-url', 'some-data', noop);
|
|
xhr = MockXhr.$$lastInstance;
|
|
|
|
expect(xhr.$$method).toBe('GET');
|
|
expect(xhr.$$url).toBe('/some-url');
|
|
expect(xhr.$$data).toBe('some-data');
|
|
expect(xhr.$$async).toBe(true);
|
|
});
|
|
|
|
it('should pass null to send if no body is set', function() {
|
|
$backend('GET', '/some-url', null, noop);
|
|
xhr = MockXhr.$$lastInstance;
|
|
|
|
expect(xhr.$$data).toBe(null);
|
|
});
|
|
|
|
it('should call completion function with xhr.statusText if present', function() {
|
|
callback.andCallFake(function(status, response, headers, statusText) {
|
|
expect(statusText).toBe('OK');
|
|
});
|
|
|
|
$backend('GET', '/some-url', null, callback);
|
|
xhr = MockXhr.$$lastInstance;
|
|
xhr.statusText = 'OK';
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('should call completion function with empty string if not present', function() {
|
|
callback.andCallFake(function(status, response, headers, statusText) {
|
|
expect(statusText).toBe('');
|
|
});
|
|
|
|
$backend('GET', '/some-url', null, callback);
|
|
xhr = MockXhr.$$lastInstance;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
|
|
it('should normalize IE\'s 1223 status code into 204', function() {
|
|
callback.andCallFake(function(status) {
|
|
expect(status).toBe(204);
|
|
});
|
|
|
|
$backend('GET', 'URL', null, callback);
|
|
xhr = MockXhr.$$lastInstance;
|
|
|
|
xhr.status = 1223;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
// onreadystatechange might by called multiple times
|
|
// with readyState === 4 on mobile webkit caused by
|
|
// xhrs that are resolved while the app is in the background (see #5426).
|
|
it('should not process onreadystatechange callback with readyState == 4 more than once', function() {
|
|
$backend('GET', 'URL', null, callback);
|
|
xhr = MockXhr.$$lastInstance;
|
|
|
|
xhr.status = 200;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
xhr.onreadystatechange();
|
|
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('should set only the requested headers', function() {
|
|
$backend('POST', 'URL', null, noop, {'X-header1': 'value1', 'X-header2': 'value2'});
|
|
xhr = MockXhr.$$lastInstance;
|
|
|
|
expect(xhr.$$reqHeaders).toEqual({
|
|
'X-header1': 'value1',
|
|
'X-header2': 'value2'
|
|
});
|
|
});
|
|
|
|
it('should set requested headers even if they have falsy values', function() {
|
|
$backend('POST', 'URL', null, noop, {
|
|
'X-header1': 0,
|
|
'X-header2': '',
|
|
'X-header3': false,
|
|
'X-header4': undefined
|
|
});
|
|
|
|
xhr = MockXhr.$$lastInstance;
|
|
|
|
expect(xhr.$$reqHeaders).toEqual({
|
|
'X-header1': 0,
|
|
'X-header2': '',
|
|
'X-header3': false
|
|
});
|
|
});
|
|
|
|
it('should not try to read response data when request is aborted', function() {
|
|
callback.andCallFake(function(status, response, headers) {
|
|
expect(status).toBe(-1);
|
|
expect(response).toBe(null);
|
|
expect(headers).toBe(null);
|
|
});
|
|
$backend('GET', '/url', null, callback, {}, 2000);
|
|
xhr = MockXhr.$$lastInstance;
|
|
spyOn(xhr, 'abort');
|
|
|
|
fakeTimeout.flush();
|
|
expect(xhr.abort).toHaveBeenCalledOnce();
|
|
|
|
xhr.status = 0;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('should abort request on timeout', function() {
|
|
callback.andCallFake(function(status, response) {
|
|
expect(status).toBe(-1);
|
|
});
|
|
|
|
$backend('GET', '/url', null, callback, {}, 2000);
|
|
xhr = MockXhr.$$lastInstance;
|
|
spyOn(xhr, 'abort');
|
|
|
|
expect(fakeTimeout.delays[0]).toBe(2000);
|
|
|
|
fakeTimeout.flush();
|
|
expect(xhr.abort).toHaveBeenCalledOnce();
|
|
|
|
xhr.status = 0;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
|
|
it('should abort request on timeout promise resolution', inject(function($timeout) {
|
|
callback.andCallFake(function(status, response) {
|
|
expect(status).toBe(-1);
|
|
});
|
|
|
|
$backend('GET', '/url', null, callback, {}, $timeout(noop, 2000));
|
|
xhr = MockXhr.$$lastInstance;
|
|
spyOn(xhr, 'abort');
|
|
|
|
$timeout.flush();
|
|
expect(xhr.abort).toHaveBeenCalledOnce();
|
|
|
|
xhr.status = 0;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
}));
|
|
|
|
|
|
it('should not abort resolved request on timeout promise resolution', inject(function($timeout) {
|
|
callback.andCallFake(function(status, response) {
|
|
expect(status).toBe(200);
|
|
});
|
|
|
|
$backend('GET', '/url', null, callback, {}, $timeout(noop, 2000));
|
|
xhr = MockXhr.$$lastInstance;
|
|
spyOn(xhr, 'abort');
|
|
|
|
xhr.status = 200;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
|
|
$timeout.flush();
|
|
expect(xhr.abort).not.toHaveBeenCalled();
|
|
}));
|
|
|
|
|
|
it('should cancel timeout on completion', function() {
|
|
callback.andCallFake(function(status, response) {
|
|
expect(status).toBe(200);
|
|
});
|
|
|
|
$backend('GET', '/url', null, callback, {}, 2000);
|
|
xhr = MockXhr.$$lastInstance;
|
|
spyOn(xhr, 'abort');
|
|
|
|
expect(fakeTimeout.delays[0]).toBe(2000);
|
|
|
|
xhr.status = 200;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
|
|
expect(fakeTimeout.delays.length).toBe(0);
|
|
expect(xhr.abort).not.toHaveBeenCalled();
|
|
});
|
|
|
|
|
|
it('should register onreadystatechange callback before sending', function() {
|
|
// send() in IE6, IE7 is sync when serving from cache
|
|
function SyncXhr() {
|
|
xhr = this;
|
|
this.open = this.setRequestHeader = noop;
|
|
|
|
this.send = function() {
|
|
this.status = 200;
|
|
this.responseText = 'response';
|
|
this.readyState = 4;
|
|
this.onreadystatechange();
|
|
};
|
|
|
|
this.getAllResponseHeaders = valueFn('');
|
|
}
|
|
|
|
callback.andCallFake(function(status, response) {
|
|
expect(status).toBe(200);
|
|
expect(response).toBe('response');
|
|
});
|
|
|
|
$backend = createHttpBackend($browser, function() { return new SyncXhr() });
|
|
$backend('GET', '/url', null, callback);
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
|
|
it('should set withCredentials', function() {
|
|
$backend('GET', '/some.url', null, callback, {}, null, true);
|
|
expect(MockXhr.$$lastInstance.withCredentials).toBe(true);
|
|
});
|
|
|
|
|
|
describe('responseType', function() {
|
|
|
|
it('should set responseType and return xhr.response', function() {
|
|
$backend('GET', '/whatever', null, callback, {}, null, null, 'blob');
|
|
|
|
var xhrInstance = MockXhr.$$lastInstance;
|
|
expect(xhrInstance.responseType).toBe('blob');
|
|
|
|
callback.andCallFake(function(status, response) {
|
|
expect(response).toBe(xhrInstance.response);
|
|
});
|
|
|
|
xhrInstance.response = {some: 'object'};
|
|
xhrInstance.readyState = 4;
|
|
xhrInstance.onreadystatechange();
|
|
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
|
|
it('should read responseText if response was not defined', function() {
|
|
// old browsers like IE8, don't support responseType, so they always respond with responseText
|
|
|
|
$backend('GET', '/whatever', null, callback, {}, null, null, 'blob');
|
|
|
|
var xhrInstance = MockXhr.$$lastInstance;
|
|
var responseText = '{"some": "object"}';
|
|
expect(xhrInstance.responseType).toBe('blob');
|
|
|
|
callback.andCallFake(function(status, response) {
|
|
expect(response).toBe(responseText);
|
|
});
|
|
|
|
xhrInstance.responseText = responseText;
|
|
xhrInstance.readyState = 4;
|
|
xhrInstance.onreadystatechange();
|
|
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
});
|
|
|
|
|
|
describe('JSONP', function() {
|
|
|
|
var SCRIPT_URL = /([^\?]*)\?cb=angular\.callbacks\.(.*)/;
|
|
|
|
|
|
it('should add script tag for JSONP request', function() {
|
|
callback.andCallFake(function(status, response) {
|
|
expect(status).toBe(200);
|
|
expect(response).toBe('some-data');
|
|
});
|
|
|
|
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
|
|
expect(fakeDocument.$$scripts.length).toBe(1);
|
|
|
|
var script = fakeDocument.$$scripts.shift(),
|
|
url = script.src.match(SCRIPT_URL);
|
|
|
|
expect(url[1]).toBe('http://example.org/path');
|
|
callbacks[url[2]]('some-data');
|
|
|
|
if (script.onreadystatechange) {
|
|
script.readyState = 'complete';
|
|
script.onreadystatechange();
|
|
} else {
|
|
browserTrigger(script, "load");
|
|
}
|
|
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
|
|
it('should clean up the callback and remove the script', function() {
|
|
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
|
|
expect(fakeDocument.$$scripts.length).toBe(1);
|
|
|
|
|
|
var script = fakeDocument.$$scripts.shift(),
|
|
callbackId = script.src.match(SCRIPT_URL)[2];
|
|
|
|
callbacks[callbackId]('some-data');
|
|
if (script.onreadystatechange) {
|
|
script.readyState = 'complete';
|
|
script.onreadystatechange();
|
|
} else {
|
|
browserTrigger(script, "load");
|
|
}
|
|
|
|
expect(callbacks[callbackId]).toBe(angular.noop);
|
|
expect(fakeDocument.body.removeChild).toHaveBeenCalledOnceWith(script);
|
|
});
|
|
|
|
|
|
if (msie <= 8) {
|
|
|
|
it('should attach onreadystatechange handler to the script object', function() {
|
|
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, noop);
|
|
|
|
expect(fakeDocument.$$scripts[0].onreadystatechange).toEqual(jasmine.any(Function));
|
|
|
|
var script = fakeDocument.$$scripts[0];
|
|
|
|
script.readyState = 'complete';
|
|
script.onreadystatechange();
|
|
|
|
expect(script.onreadystatechange).toBe(null);
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
it('should set url to current location if not specified or empty string', function() {
|
|
$backend('JSONP', undefined, null, callback);
|
|
expect(fakeDocument.$$scripts[0].src).toBe($browser.url());
|
|
fakeDocument.$$scripts.shift();
|
|
|
|
$backend('JSONP', '', null, callback);
|
|
expect(fakeDocument.$$scripts[0].src).toBe($browser.url());
|
|
});
|
|
|
|
|
|
it('should abort request on timeout and replace callback with noop', function() {
|
|
callback.andCallFake(function(status, response) {
|
|
expect(status).toBe(-1);
|
|
});
|
|
|
|
$backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback, null, 2000);
|
|
expect(fakeDocument.$$scripts.length).toBe(1);
|
|
expect(fakeTimeout.delays[0]).toBe(2000);
|
|
|
|
var script = fakeDocument.$$scripts.shift(),
|
|
callbackId = script.src.match(SCRIPT_URL)[2];
|
|
|
|
fakeTimeout.flush();
|
|
expect(fakeDocument.$$scripts.length).toBe(0);
|
|
expect(callback).toHaveBeenCalledOnce();
|
|
|
|
expect(callbacks[callbackId]).toBe(angular.noop);
|
|
});
|
|
|
|
|
|
// TODO(vojta): test whether it fires "async-start"
|
|
// TODO(vojta): test whether it fires "async-end" on both success and error
|
|
});
|
|
|
|
describe('protocols that return 0 status code', function() {
|
|
|
|
function respond(status, content) {
|
|
xhr = MockXhr.$$lastInstance;
|
|
xhr.status = status;
|
|
xhr.responseText = content;
|
|
xhr.readyState = 4;
|
|
xhr.onreadystatechange();
|
|
}
|
|
|
|
|
|
it('should convert 0 to 200 if content and file protocol', function() {
|
|
$backend = createHttpBackend($browser, createMockXhr);
|
|
|
|
$backend('GET', 'file:///whatever/index.html', null, callback);
|
|
respond(0, 'SOME CONTENT');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(200);
|
|
});
|
|
|
|
it('should convert 0 to 200 if content for protocols other than file', function() {
|
|
$backend = createHttpBackend($browser, createMockXhr);
|
|
|
|
$backend('GET', 'someProtocol:///whatever/index.html', null, callback);
|
|
respond(0, 'SOME CONTENT');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(200);
|
|
});
|
|
|
|
it('should convert 0 to 404 if no content and file protocol', function() {
|
|
$backend = createHttpBackend($browser, createMockXhr);
|
|
|
|
$backend('GET', 'file:///whatever/index.html', null, callback);
|
|
respond(0, '');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(404);
|
|
});
|
|
|
|
it('should not convert 0 to 404 if no content for protocols other than file', function() {
|
|
$backend = createHttpBackend($browser, createMockXhr);
|
|
|
|
$backend('GET', 'someProtocol:///whatever/index.html', null, callback);
|
|
respond(0, '');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(0);
|
|
});
|
|
|
|
it('should convert 0 to 404 if no content - relative url', function() {
|
|
var originalUrlParsingNode = urlParsingNode;
|
|
|
|
//temporarily overriding the DOM element to pretend that the test runs origin with file:// protocol
|
|
urlParsingNode = {
|
|
hash : "#/C:/",
|
|
host : "",
|
|
hostname : "",
|
|
href : "file:///C:/base#!/C:/foo",
|
|
pathname : "/C:/foo",
|
|
port : "",
|
|
protocol : "file:",
|
|
search : "",
|
|
setAttribute: angular.noop
|
|
};
|
|
|
|
try {
|
|
|
|
$backend = createHttpBackend($browser, createMockXhr);
|
|
|
|
$backend('GET', '/whatever/index.html', null, callback);
|
|
respond(0, '');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(404);
|
|
|
|
} finally {
|
|
urlParsingNode = originalUrlParsingNode;
|
|
}
|
|
});
|
|
|
|
|
|
it('should return original backend status code if different from 0', function () {
|
|
$backend = createHttpBackend($browser, createMockXhr);
|
|
|
|
// request to http://
|
|
$backend('POST', 'http://rest_api/create_whatever', null, callback);
|
|
respond(201, '');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(201);
|
|
|
|
|
|
// request to file://
|
|
$backend('POST', 'file://rest_api/create_whatever', null, callback);
|
|
respond(201, '');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(201);
|
|
|
|
// request to file:// with HTTP status >= 300
|
|
$backend('POST', 'file://rest_api/create_whatever', null, callback);
|
|
respond(503, '');
|
|
|
|
expect(callback).toHaveBeenCalled();
|
|
expect(callback.mostRecentCall.args[0]).toBe(503);
|
|
});
|
|
});
|
|
});
|
|
|